diff --git a/_maps/templates/holodeck_doppler_wargame_board.dmm b/_maps/templates/holodeck_doppler_wargame_board.dmm
new file mode 100644
index 00000000000000..f0e654d83f101d
--- /dev/null
+++ b/_maps/templates/holodeck_doppler_wargame_board.dmm
@@ -0,0 +1,535 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"a" = (
+/obj/effect/turf_decal/arrows/white{
+ dir = 1
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 8
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 4
+ },
+/turf/open/floor/holofloor/plating,
+/area/template_noop)
+"c" = (
+/obj/structure/railing{
+ dir = 8
+ },
+/turf/open/floor/holofloor/stairs,
+/area/template_noop)
+"d" = (
+/obj/effect/turf_decal/tile/neutral/half,
+/obj/structure/chair/sofa/bench/right,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"e" = (
+/obj/effect/turf_decal/tile/neutral/half/contrasted{
+ dir = 1
+ },
+/obj/effect/turf_decal/trimline/yellow/warning,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"f" = (
+/obj/effect/turf_decal/tile/neutral{
+ dir = 4
+ },
+/obj/structure/railing,
+/obj/effect/turf_decal/trimline/yellow/warning,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"g" = (
+/obj/effect/turf_decal/tile/neutral/half{
+ dir = 1
+ },
+/obj/structure/table,
+/obj/item/wargame_base_station{
+ pixel_y = 12;
+ pixel_x = 2
+ },
+/obj/item/wargame_projector/ships/green{
+ pixel_x = -8
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"h" = (
+/obj/effect/turf_decal/tile/neutral/half{
+ dir = 1
+ },
+/obj/structure/table,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"k" = (
+/obj/effect/turf_decal/tile/neutral/half/contrasted,
+/obj/structure/reagent_dispensers/water_cooler/punch_cooler,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"l" = (
+/obj/structure/railing{
+ dir = 4
+ },
+/turf/open/floor/holofloor/stairs{
+ dir = 1
+ },
+/area/template_noop)
+"m" = (
+/obj/effect/turf_decal/trimline/yellow/corner,
+/turf/open/floor/holofloor{
+ icon_state = "smooth_large";
+ base_icon_state = "smooth_large"
+ },
+/area/template_noop)
+"n" = (
+/obj/effect/turf_decal/tile/neutral{
+ dir = 8
+ },
+/obj/structure/chair/sofa/bench/left,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"o" = (
+/obj/structure/railing{
+ dir = 4
+ },
+/turf/open/floor/holofloor/stairs,
+/area/template_noop)
+"p" = (
+/obj/effect/turf_decal/tile/neutral/half,
+/obj/effect/turf_decal/trimline/yellow/warning{
+ dir = 1
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"q" = (
+/obj/effect/turf_decal/tile/neutral{
+ dir = 4
+ },
+/obj/structure/table,
+/obj/item/clothing/head/tajaran_hat{
+ pixel_y = 8;
+ pixel_x = -4
+ },
+/obj/item/clothing/head/lizard_hat/white{
+ pixel_x = -5
+ },
+/obj/item/wargame_projector/terrain{
+ pixel_y = 13;
+ pixel_x = 14
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"r" = (
+/obj/effect/turf_decal/tile/neutral/half/contrasted{
+ dir = 1
+ },
+/obj/item/kirbyplants/random/fullysynthetic,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"t" = (
+/obj/structure/railing{
+ dir = 8
+ },
+/turf/open/floor/holofloor/stairs{
+ dir = 1
+ },
+/area/template_noop)
+"u" = (
+/obj/effect/turf_decal/tile/neutral{
+ dir = 8
+ },
+/obj/structure/railing{
+ dir = 1
+ },
+/obj/effect/turf_decal/trimline/yellow/warning{
+ dir = 1
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"v" = (
+/obj/effect/turf_decal/trimline/yellow/corner{
+ dir = 8
+ },
+/turf/open/floor/holofloor{
+ icon_state = "smooth_large";
+ base_icon_state = "smooth_large"
+ },
+/area/template_noop)
+"w" = (
+/obj/effect/turf_decal/trimline/yellow/corner{
+ dir = 1
+ },
+/turf/open/floor/holofloor{
+ icon_state = "smooth_large";
+ base_icon_state = "smooth_large"
+ },
+/area/template_noop)
+"x" = (
+/obj/effect/turf_decal/tile/neutral,
+/obj/structure/railing{
+ dir = 1
+ },
+/obj/effect/turf_decal/trimline/yellow/warning{
+ dir = 1
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"y" = (
+/obj/effect/turf_decal/tile/neutral/half,
+/obj/structure/railing{
+ dir = 1
+ },
+/obj/effect/turf_decal/trimline/yellow/warning{
+ dir = 1
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"z" = (
+/obj/effect/turf_decal/tile/neutral/half{
+ dir = 1
+ },
+/obj/effect/turf_decal/trimline/yellow/warning,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"B" = (
+/obj/effect/turf_decal/tile/neutral{
+ dir = 1
+ },
+/obj/structure/railing,
+/obj/effect/turf_decal/trimline/yellow/warning,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"C" = (
+/obj/effect/turf_decal/tile/neutral/half{
+ dir = 1
+ },
+/obj/structure/chair/sofa/bench/solo{
+ dir = 4;
+ pixel_y = 4
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"D" = (
+/obj/effect/turf_decal/tile/neutral{
+ dir = 1
+ },
+/obj/structure/chair/sofa/bench/solo{
+ dir = 8;
+ pixel_y = 4
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"F" = (
+/turf/open/indestructible/wargame,
+/area/template_noop)
+"I" = (
+/obj/effect/turf_decal/arrows/white{
+ dir = 1
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 4
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 8
+ },
+/turf/open/floor/holofloor/plating,
+/area/template_noop)
+"J" = (
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 8
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 4
+ },
+/turf/open/floor/holofloor/plating,
+/area/template_noop)
+"L" = (
+/obj/effect/turf_decal/tile/neutral/half,
+/obj/structure/chair/sofa/bench,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"M" = (
+/obj/effect/turf_decal/tile/neutral/half,
+/obj/structure/chair/sofa/bench/left,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"O" = (
+/obj/effect/turf_decal/arrows/white,
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 8
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 4
+ },
+/turf/open/floor/holofloor/plating,
+/area/template_noop)
+"P" = (
+/obj/effect/turf_decal/tile/neutral/half{
+ dir = 1
+ },
+/obj/structure/railing,
+/obj/effect/turf_decal/trimline/yellow/warning,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"Q" = (
+/obj/item/kirbyplants/random/fullysynthetic,
+/turf/open/floor/holofloor{
+ icon_state = "smooth_large";
+ base_icon_state = "smooth_large"
+ },
+/area/template_noop)
+"R" = (
+/obj/effect/turf_decal/tile/neutral/half{
+ dir = 1
+ },
+/obj/structure/shelf,
+/obj/item/wargame_projector/ships/red{
+ pixel_x = 9;
+ pixel_y = 4
+ },
+/obj/item/wargame_projector/ships{
+ pixel_y = 12
+ },
+/obj/item/wargame_projector/ships/yellow{
+ pixel_y = -1
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth_half";
+ icon_state = "smooth_half";
+ dir = 4
+ },
+/area/template_noop)
+"S" = (
+/obj/effect/turf_decal/tile/neutral,
+/obj/structure/chair/sofa/bench/right,
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+"V" = (
+/obj/effect/turf_decal/arrows/white,
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 4
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 8
+ },
+/turf/open/floor/holofloor/plating,
+/area/template_noop)
+"W" = (
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 4
+ },
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 8
+ },
+/turf/open/floor/holofloor/plating,
+/area/template_noop)
+"X" = (
+/obj/effect/turf_decal/trimline/yellow/corner{
+ dir = 4
+ },
+/turf/open/floor/holofloor{
+ icon_state = "smooth_large";
+ base_icon_state = "smooth_large"
+ },
+/area/template_noop)
+"Y" = (
+/obj/effect/turf_decal/tile/neutral/half/contrasted,
+/obj/effect/turf_decal/trimline/yellow/warning{
+ dir = 1
+ },
+/turf/open/floor/holofloor{
+ base_icon_state = "smooth";
+ icon_state = "smooth";
+ dir = 1
+ },
+/area/template_noop)
+
+(1,1,1) = {"
+Q
+m
+o
+I
+W
+W
+V
+l
+X
+Q
+"}
+(2,1,1) = {"
+S
+f
+F
+F
+F
+F
+F
+F
+x
+q
+"}
+(3,1,1) = {"
+L
+P
+F
+F
+F
+F
+F
+F
+y
+g
+"}
+(4,1,1) = {"
+M
+z
+F
+F
+F
+F
+F
+F
+p
+R
+"}
+(5,1,1) = {"
+k
+e
+F
+F
+F
+F
+F
+F
+Y
+r
+"}
+(6,1,1) = {"
+d
+z
+F
+F
+F
+F
+F
+F
+p
+C
+"}
+(7,1,1) = {"
+L
+P
+F
+F
+F
+F
+F
+F
+y
+h
+"}
+(8,1,1) = {"
+n
+B
+F
+F
+F
+F
+F
+F
+u
+D
+"}
+(9,1,1) = {"
+Q
+v
+c
+a
+J
+J
+O
+t
+w
+Q
+"}
diff --git a/code/__DEFINES/~doppler_defines/wargames.dm b/code/__DEFINES/~doppler_defines/wargames.dm
new file mode 100644
index 00000000000000..e81f0f6ac103e6
--- /dev/null
+++ b/code/__DEFINES/~doppler_defines/wargames.dm
@@ -0,0 +1,62 @@
+#define WARGAME_ACTIONS_FILE 'modular_doppler/wargaming/icons/actions.dmi'
+
+/// Current game phase is placing units
+#define WARGAME_PHASE_PLACEMENT 0
+/// Current game phase is action
+#define WARGAME_PHASE_ACTION 1
+/// Current game phase is effects
+#define WARGAME_PHASE_EFFECTS 2
+/// Current game phase is nothing, the game is over or hasn't even started yet
+#define WARGAME_PHASE_NOTHING 10
+
+/// Something like a dust cloud that gives an evasion bonus but no cover
+#define WARGAME_EVASION_BONUS 0
+/// A small thing with zero cover value
+#define WARGAME_SIZE_SMALL 1
+/// A medium sized thing that small things can hide around
+#define WARGAME_SIZE_MEDIUM 2
+/// A large thing that even decent sized ships can hide around
+#define WARGAME_SIZE_LARGE 3
+
+/// The maximum amount of cover bonus a unit can have
+#define WARGAME_MAX_COVER_BONUS 2
+/// The maximum amount of evasion bonus a unit can have
+#define WARGAME_MAX_EVASION_BONUS 1
+
+/// Add a team to the base station
+#define BASESTATION_ADD_TEAM "Add Team"
+/// Remove a team from the base station
+#define BASESTATION_REMOVE_TEAM "Remove Team"
+/// Edit a team on the base station
+#define BASESTATION_EDIT_TEAM "Edit Team"
+/// Allows the user to join or leave a team
+#define BASESTATION_JOIN_LEAVE "Join/Leave Team"
+/// Reset the basestation, removing all teams and configuration
+#define BASESTATION_RESET "Reset Configuration"
+/// Start the game with all current base station settings
+#define BASESTATION_START "Start Game"
+/// Immediately stop the currently running game on the base station
+#define BASESTATION_END "End Game"
+/// End the current phase and advance to the next, changing to the next team's turn if applicable
+#define BASESTATION_NEXT "Next Phase"
+
+/// Change the name of the currently selected team
+#define BASESTATION_TEAM_NAME "Team Name"
+/// Change the color of the currently selected team
+#define BASESTATION_TEAM_COLOR "Team Color"
+/// Change a team's units address them
+#define BASESTATION_TEAM_COMMANDER "Team Commander Response"
+/// Change a team's name prefix for automatically generated ship names
+#define BASESTATION_TEAM_PREFIX "Team Ship Prefix"
+
+/// Opens a unit's movement options
+#define WARGAME_UNIT_MOVE "Move"
+/// Opens a unit's attack options
+#define WARGAME_UNIT_ATTACK "Attack"
+/// Opens a unit's special options
+#define WARGAME_UNIT_SPECIAL "Special"
+
+/// If the base station will go one team at a time until the effects phase
+#define WARGAME_TURN_MODE_STANDARD "Sequential"
+/// If the base station will let every team move at once before the effects phase
+#define WARGAME_TURN_MODE_SIMULTANEOUS "Simultaneous"
diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm
index 8c4753644bbd84..0dc8596a2f0ad3 100644
--- a/code/datums/chatmessage.dm
+++ b/code/datums/chatmessage.dm
@@ -164,7 +164,7 @@
chat_color_name_to_use = target.get_voice() // for everything else, use the target's voice name
// Calculate target color if not already present
- if (!target.chat_color || target.chat_color_name != chat_color_name_to_use)
+ if ((!target.chat_color || target.chat_color_name != chat_color_name_to_use) && !target.do_not_override_chat_colors) // DOPPLER EDIT - Adds option to not override chat colors - ORIGINAL: if (!target.chat_color || target.chat_color_name != chat_color_name_to_use)
target.chat_color = get_chat_color_string(chat_color_name_to_use) // DOPPLER EDIT CHANGE - ORIGINAL: target.chat_color = colorize_string(chat_color_name_to_use)
target.chat_color_darkened = get_chat_color_string(chat_color_name_to_use, darkened = TRUE) // DOPPLER EDIT CHANGE - ORIGINAL: target.chat_color_darkened = colorize_string(chat_color_name_to_use, 0.85, 0.85)
target.chat_color_name = chat_color_name_to_use
diff --git a/code/modules/holodeck/computer.dm b/code/modules/holodeck/computer.dm
index 11c006047f2da4..8f348390679152 100644
--- a/code/modules/holodeck/computer.dm
+++ b/code/modules/holodeck/computer.dm
@@ -27,7 +27,7 @@ and clear when youre done! if you dont i will use :newspaper2: on you
/// typecache for turfs that should be considered ok during floorchecks.
/// A linked turf being anything not in this typecache will cause the holodeck to perform an emergency shutdown.
-GLOBAL_LIST_INIT(typecache_holodeck_linked_floorcheck_ok, typecacheof(list(/turf/open/floor/holofloor, /turf/closed)))
+GLOBAL_LIST_INIT(typecache_holodeck_linked_floorcheck_ok, typecacheof(list(/turf/open/floor/holofloor, /turf/closed, /turf/open/indestructible/wargame))) // DOPPLER EDIT - Wargame holodeck - ORIGINAL: GLOBAL_LIST_INIT(typecache_holodeck_linked_floorcheck_ok, typecacheof(list(/turf/open/floor/holofloor, /turf/closed)))
/obj/machinery/computer/holodeck
name = "holodeck control console"
diff --git a/modular_doppler/loadout_categories/categories/toys.dm b/modular_doppler/loadout_categories/categories/toys.dm
index f5db2bca436e8b..6df9a9cca7f32b 100644
--- a/modular_doppler/loadout_categories/categories/toys.dm
+++ b/modular_doppler/loadout_categories/categories/toys.dm
@@ -85,6 +85,10 @@
item_path = /obj/item/instrument/harmonica
restricted_roles = list(JOB_PRISONER)
+/datum/loadout_item/toy/wargame_kit
+ name = "Portable Holographic Wargame Kit"
+ item_path = /obj/item/storage/briefcase/secure/wargame_kit
+
/// Plushie blast
/datum/loadout_item/toy/plush
diff --git a/modular_doppler/modular_customization/preferences/chat_color.dm b/modular_doppler/modular_customization/preferences/chat_color.dm
index b8c117c2de55c2..5f3d317dcf6023 100644
--- a/modular_doppler/modular_customization/preferences/chat_color.dm
+++ b/modular_doppler/modular_customization/preferences/chat_color.dm
@@ -1,3 +1,7 @@
+/atom
+ /// Tells the chat message color system to not override this thing's chat color with the cache
+ var/do_not_override_chat_colors = FALSE
+
/datum/preference/color/chat_color
category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
priority = PREFERENCE_PRIORITY_NAME_MODIFICATIONS
diff --git a/modular_doppler/wargaming/code/base_station.dm b/modular_doppler/wargaming/code/base_station.dm
new file mode 100644
index 00000000000000..ff62da626101b6
--- /dev/null
+++ b/modular_doppler/wargaming/code/base_station.dm
@@ -0,0 +1,462 @@
+/obj/item/wargame_base_station
+ name = "wargames base station"
+ desc = "A base station for holographic wargames, all controllers and interactive wearables link to this machine and are automatically managed by it."
+ icon = 'modular_doppler/wargaming/icons/items.dmi'
+ icon_state = "basestation"
+ lefthand_file = 'modular_doppler/wargaming/icons/mob/lefthand.dmi'
+ righthand_file = 'modular_doppler/wargaming/icons/mob/righthand.dmi'
+ inhand_icon_state = "generic"
+ worn_icon = 'modular_doppler/wargaming/icons/mob/worn.dmi'
+ worn_icon_state = "controller"
+ w_class = WEIGHT_CLASS_SMALL
+ item_flags = NOBLUDGEON
+ drop_sound = 'sound/items/handling/tools/rcd_drop.ogg'
+ pickup_sound = 'sound/items/handling/tools/rcd_pickup.ogg'
+ do_not_override_chat_colors = TRUE
+ slot_flags = ITEM_SLOT_BELT
+ chat_color = LIGHT_COLOR_COPPER_OXIDE
+ /// List of all team datums this controller is currently managing, empty teams are cleared at game start
+ var/list/managed_teams = list()
+ /// Nebulous list of teams per round, subtracted from as teams complete their turns
+ var/list/teams_per_turn = list()
+ /// What phase of the game are we currently in
+ var/game_phase = WARGAME_PHASE_NOTHING
+ /// The turn mode we are using, one team at a time or all at once
+ var/turn_mode = WARGAME_TURN_MODE_STANDARD
+ /// Which team's turn is it right now?
+ var/datum/wargaming_team/team_turn
+ /// How many turns have passed
+ var/turn_counter = 1
+ /// List of any terrain projectors linked to us
+ var/list/terrain_projectors = list()
+ /// Radial options for before the game has been started
+ var/static/list/pre_game_radial_options = list(
+ BASESTATION_JOIN_LEAVE = image(icon = WARGAME_ACTIONS_FILE, icon_state = "join_team"),
+ BASESTATION_ADD_TEAM = image(icon = WARGAME_ACTIONS_FILE, icon_state = "add_team"),
+ BASESTATION_REMOVE_TEAM = image(icon = WARGAME_ACTIONS_FILE, icon_state = "delete_team"),
+ BASESTATION_EDIT_TEAM = image(icon = WARGAME_ACTIONS_FILE, icon_state = "edit_team"),
+ BASESTATION_RESET = image(icon = WARGAME_ACTIONS_FILE, icon_state = "reset"),
+ BASESTATION_START = image(icon = WARGAME_ACTIONS_FILE, icon_state = "start_game"),
+ )
+ /// Radial options for after the game has already been started
+ var/static/list/mid_game_radial_options = list(
+ BASESTATION_END = image(icon = WARGAME_ACTIONS_FILE, icon_state = "end_game"),
+ BASESTATION_JOIN_LEAVE = image(icon = WARGAME_ACTIONS_FILE, icon_state = "join_team"),
+ BASESTATION_NEXT = image(icon = WARGAME_ACTIONS_FILE, icon_state = "next_phase"),
+ )
+ /// Radial options for editing a team
+ var/static/list/team_edit_radial_options = list(
+ BASESTATION_TEAM_NAME = image(icon = WARGAME_ACTIONS_FILE, icon_state = "team_name"),
+ BASESTATION_TEAM_COLOR = image(icon = WARGAME_ACTIONS_FILE, icon_state = "team_color"),
+ BASESTATION_TEAM_COMMANDER = image(icon = WARGAME_ACTIONS_FILE, icon_state = "team_commander"),
+ BASESTATION_TEAM_PREFIX = image(icon = WARGAME_ACTIONS_FILE, icon_state = "team_prefix"),
+ )
+
+/obj/item/wargame_base_station/Initialize(mapload)
+ . = ..()
+ register_context()
+ if(prob(0.01))
+ make_special_edition()
+
+/obj/item/wargame_base_station/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Open console menu"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/item/wargame_base_station/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, "basestation_emissive", src, alpha = 100)
+
+/// Turns the console gold and gives it a silly name
+/obj/item/wargame_base_station/proc/make_special_edition()
+ name = "\improper DopplerStation 2"
+ desc = "A base station for holographic wargames, all controllers and interactive wearables link to this machine and are automatically managed by it. \
+ This looks like the special pre-order edition of the console, how did one of these get out this far? The faceplate has been replaced with one \
+ plated in gold, and someone seems to have left a sticker of a little dolphin on the case. \
+ Whoever owned this before must lament their purchase every day, this thing only has one game on it!"
+ icon_state = "basestation_gold"
+ update_appearance(UPDATE_ICON)
+
+/obj/item/wargame_base_station/examine(mob/user)
+ . = ..()
+ . += span_notice("Interact with the menu with [EXAMINE_HINT("Alt-Click")].")
+ // Game stats
+ var/game_stats_text = ""
+ game_stats_text += "The game is currently in the [game_phase_2_text(game_phase)] phase.
"
+ if(turn_mode == WARGAME_TURN_MODE_SIMULTANEOUS)
+ game_stats_text += "The game is in simultaneous turns mode, all teams may make actions.
"
+ else if(team_turn)
+ game_stats_text += "It is the turn of team [team_turn.team_name].
"
+ game_stats_text += game_phase_2_desc(game_phase)
+ game_stats_text += "The teams have completed [EXAMINE_HINT("[turn_counter - 1]")] total turns.
"
+ . += fieldset_block(game_phase_2_text(game_phase, TRUE), game_stats_text, "boxed_message")
+ // Player and team stats
+ for(var/team as anything in managed_teams)
+ var/datum/wargaming_team/team_datum = managed_teams[team]
+ var/team_display = ""
+ if(length(team_datum.team_players))
+ team_display += "Composed of [length(team_datum.team_players)] players:
"
+ for(var/mob/living/player_on_team as anything in team_datum.team_players)
+ team_display += " - [player_on_team.get_visible_name()]
"
+ else
+ team_display = "Team currently empty."
+ . += fieldset_block(span_bold(team), team_display, "boxed_message")
+
+/obj/item/wargame_base_station/click_alt(mob/living/user)
+ var/picked_choice = show_radial_menu(user, src, (game_phase == WARGAME_PHASE_NOTHING) ? pre_game_radial_options : mid_game_radial_options, require_near = TRUE, tooltips = TRUE)
+ if(isnull(picked_choice))
+ play_menu_sound()
+ return CLICK_ACTION_BLOCKING
+ switch(picked_choice)
+ // Pregame options
+ if(BASESTATION_JOIN_LEAVE)
+ play_menu_sound()
+ try_join_leave(user)
+ if(BASESTATION_ADD_TEAM)
+ play_menu_sound()
+ create_new_team(user)
+ if(BASESTATION_REMOVE_TEAM)
+ play_menu_sound()
+ delete_team(user)
+ if(BASESTATION_EDIT_TEAM)
+ play_menu_sound()
+ edit_a_team(user)
+ if(BASESTATION_RESET)
+ play_menu_sound()
+ reset_everything(user)
+ if(BASESTATION_START)
+ play_menu_sound()
+ start_the_game(user)
+ // Midgame options
+ if(BASESTATION_END)
+ play_menu_sound()
+ end_the_game(user)
+ if(BASESTATION_NEXT)
+ play_menu_sound()
+ advance_phase(user)
+ return CLICK_ACTION_SUCCESS
+
+/// Converts a given game phase number to text
+/obj/item/wargame_base_station/proc/game_phase_2_text(phase, capital)
+ switch(phase)
+ if(WARGAME_PHASE_PLACEMENT)
+ return capital ? "Placement" : "placement"
+ if(WARGAME_PHASE_ACTION)
+ return capital ? "Action" : "action"
+ if(WARGAME_PHASE_EFFECTS)
+ return capital ? "Effects" : "effects"
+ if(WARGAME_PHASE_NOTHING)
+ return capital ? "Pre-Game" : "pre-game"
+
+/// Converts a given game phase number to a description of it
+/obj/item/wargame_base_station/proc/game_phase_2_desc(phase)
+ switch(phase)
+ if(WARGAME_PHASE_PLACEMENT)
+ return "All teams should use their hologram projectors to create their fleets during this phase, this will be the only opportunity \
+ to use the projectors. If battlespace terrain and hazards are desired, the tertiary \"Terrain\" projector should be linked and \
+ used instead.
"
+ if(WARGAME_PHASE_ACTION)
+ return "All units on the team who's turn it currently is are allowed to use their action points to move, attack, etc. Events such as \
+ ship destruction will not take place until the next phase.
"
+ if(WARGAME_PHASE_EFFECTS)
+ return "In this phase, effects such as ships exploding due to damage are processed. This may take up to a few seconds per event, and \
+ control to skip to the next phase will be locked until all effects have been handled.
"
+ if(WARGAME_PHASE_NOTHING)
+ return "The game is currently in the pre-game lobby, nothing will happen until the \"Start Game\" option has been chosen in the menu. \
+ In order to start, the game needs at least two teams with at least one person each. These do not need to be different people, allowing \
+ for solo play.
"
+
+/// Updates every hologram run by a terrain projector
+/obj/item/wargame_base_station/proc/update_terrain_holograms()
+ for(var/obj/item/wargame_projector/projector as anything in terrain_projectors)
+ for(var/obj/structure/wargame_hologram/hologram as anything in projector.projections)
+ if(hologram.controllable)
+ hologram.unit_stats.effects_phase_process(hologram)
+
+#define MY_CHILD_WILL "Yes"
+#define MY_CHILD_WILL_NOT "No"
+
+#define JOIN_A_TEAM "Join"
+#define LEAVE_A_TEAM "Leave"
+
+/// Allows the user to join or leave a team of their choosing
+/obj/item/wargame_base_station/proc/try_join_leave(mob/living/user)
+ var/option = tgui_alert(user, "Join or leave a team?", "Team Manager", list(JOIN_A_TEAM, LEAVE_A_TEAM))
+ if(isnull(option))
+ balloon_alert(user, "no choice made!")
+ play_menu_sound()
+ return
+ play_menu_sound()
+ switch(option)
+ if(JOIN_A_TEAM)
+ var/picked_team = tgui_input_list(user, "Choose which team to join.", "Team Manager", managed_teams)
+ if(isnull(picked_team))
+ balloon_alert(user, "no choice made!")
+ play_menu_sound()
+ return
+ var/datum/wargaming_team/wargame_team = managed_teams[picked_team]
+ if(user in wargame_team.team_players)
+ balloon_alert(user, "already in!")
+ play_menu_sound()
+ return
+ wargame_team.team_players += user
+ play_menu_sound()
+ if(LEAVE_A_TEAM)
+ var/picked_team = tgui_input_list(user, "Choose which team to leave.", "Team Manager", managed_teams)
+ if(isnull(picked_team))
+ balloon_alert(user, "no choice made!")
+ play_menu_sound()
+ return
+ var/datum/wargaming_team/wargame_team = managed_teams[picked_team]
+ if(!(user in wargame_team.team_players))
+ balloon_alert(user, "not in team!")
+ play_menu_sound()
+ return
+ wargame_team.team_players -= user
+ play_menu_sound()
+
+#undef JOIN_A_TEAM
+#undef LEAVE_A_TEAM
+
+/// Advances to the next phase, changing to the next team in the list
+/obj/item/wargame_base_station/proc/advance_phase(mob/living/user)
+ if(game_phase == WARGAME_PHASE_NOTHING)
+ balloon_alert(user, "not running!")
+ return // ??
+ var/certainty = tgui_alert(user, "Advance to the next game phase?", "Advance Phase", list(MY_CHILD_WILL, MY_CHILD_WILL_NOT))
+ if(certainty != MY_CHILD_WILL)
+ return
+ switch(game_phase)
+ if(WARGAME_PHASE_PLACEMENT)
+ switch(turn_mode)
+ if(WARGAME_TURN_MODE_STANDARD)
+ team_turn.ready_all_units()
+ say("Action phase, [team_turn.team_name], turn [turn_counter].")
+ if(WARGAME_TURN_MODE_SIMULTANEOUS)
+ for(var/datum/wargaming_team/starting_team as anything in just_team_datums())
+ starting_team.ready_all_units()
+ say("Action phase, all teams, turn [turn_counter].")
+ game_phase = WARGAME_PHASE_ACTION
+ if(WARGAME_PHASE_ACTION)
+ if(!length(teams_per_turn) || turn_mode == WARGAME_TURN_MODE_SIMULTANEOUS)
+ update_terrain_holograms()
+ say("Effects phase, turn [turn_counter].")
+ for(var/datum/wargaming_team/team as anything in just_team_datums())
+ team.update_all_units()
+ game_phase = WARGAME_PHASE_EFFECTS
+ else
+ game_phase = WARGAME_PHASE_ACTION
+ team_turn = teams_per_turn[1]
+ team_turn.ready_all_units()
+ teams_per_turn -= team_turn
+ say("Action phase, [team_turn.team_name], turn [turn_counter].")
+ if(WARGAME_PHASE_EFFECTS)
+ turn_counter += 1
+ say("Turn [turn_counter - 1] complete, beginning turn [turn_counter].")
+ switch(turn_mode)
+ if(WARGAME_TURN_MODE_STANDARD)
+ teams_per_turn = just_team_datums()
+ team_turn = teams_per_turn[1]
+ team_turn.ready_all_units()
+ teams_per_turn -= team_turn
+ say("Action phase, [team_turn.team_name], turn [turn_counter].")
+ if(WARGAME_TURN_MODE_SIMULTANEOUS)
+ for(var/datum/wargaming_team/starting_team as anything in just_team_datums())
+ starting_team.ready_all_units()
+ say("Action phase, all teams, turn [turn_counter].")
+ game_phase = WARGAME_PHASE_ACTION
+ play_menu_sound()
+
+/// Ends the game if it is currently running
+/obj/item/wargame_base_station/proc/end_the_game(mob/living/user)
+ if(game_phase == WARGAME_PHASE_NOTHING)
+ balloon_alert(user, "already stopped!")
+ return
+ var/certainty = tgui_alert(user, "Are you certain you wish to end the game?", "Game Ender", list(MY_CHILD_WILL, MY_CHILD_WILL_NOT))
+ if(certainty != MY_CHILD_WILL)
+ play_menu_sound()
+ return
+ game_phase = WARGAME_PHASE_NOTHING
+ play_menu_sound()
+ say("Game ended.")
+
+/// Starts the game if it has not already been started
+/obj/item/wargame_base_station/proc/start_the_game(mob/living/user)
+ // Verify that the game can actually be started or if it has already started
+ if(game_phase != WARGAME_PHASE_NOTHING)
+ balloon_alert(user, "already started!")
+ return
+ var/number_of_valid_teams = 0
+ for(var/team as anything in managed_teams)
+ var/datum/wargaming_team/team_datum = managed_teams[team]
+ if(length(team_datum.team_players))
+ number_of_valid_teams++
+ if(number_of_valid_teams <= 1) // This counts teams that actually have players in them
+ balloon_alert(user, "too few teams!")
+ return
+ // Check if we actually want to start the game
+ var/certainty = tgui_alert(user, "Are you certain you wish to start the game?", "Game Starter", list(MY_CHILD_WILL, MY_CHILD_WILL_NOT))
+ if(certainty != MY_CHILD_WILL)
+ play_menu_sound()
+ return
+ // Select what turn mode to use, sequential means one team goes at a time, simultaneous means all teams can move at once.
+ var/turn_mode_choice = tgui_alert(user, "Select turn mode. Sequential: One team making actions at a time, rotating. Simultaneous: All teams act at once.", "Turn Mode", list(WARGAME_TURN_MODE_STANDARD, WARGAME_TURN_MODE_SIMULTANEOUS))
+ if(isnull(turn_mode_choice))
+ balloon_alert(user, "no choice made!")
+ play_menu_sound()
+ return
+ turn_mode = turn_mode_choice
+ // Actually start the game now
+ game_phase = WARGAME_PHASE_PLACEMENT
+ for(var/team as anything in managed_teams)
+ var/datum/wargaming_team/team_datum = managed_teams[team]
+ if(!length(team_datum.team_players))
+ qdel(team_datum)
+ managed_teams -= team // Empty teams are wiped before the game starts
+ if(turn_mode == WARGAME_TURN_MODE_STANDARD)
+ shuffle(managed_teams)
+ teams_per_turn = just_team_datums()
+ team_turn = teams_per_turn[1]
+ teams_per_turn -= team_turn
+ turn_counter = 1
+ play_menu_sound()
+ say("Beginning game, placement phase.")
+
+/// Returns a list of just team datums from the managed_teams list
+/obj/item/wargame_base_station/proc/just_team_datums()
+ var/team_list = list()
+ for(var/team as anything in managed_teams)
+ team_list += managed_teams[team]
+ return team_list
+
+/// Asks for confirmation before deleting every team
+/obj/item/wargame_base_station/proc/reset_everything(mob/living/user)
+ var/certainty = tgui_alert(user, "Are you certain you wish to delete every team?", "Deletion Confirmation", list(MY_CHILD_WILL, MY_CHILD_WILL_NOT))
+ if(certainty == MY_CHILD_WILL)
+ for(var/team as anything in managed_teams)
+ qdel(managed_teams[team])
+ managed_teams -= team
+ play_menu_sound()
+
+/// Selects a team and edits the name or color
+/obj/item/wargame_base_station/proc/edit_a_team(mob/living/user)
+ var/editing_team = tgui_input_list(user, "Choose which team you wish to edit.", "Team List", managed_teams)
+ if(isnull(editing_team))
+ balloon_alert(user, "no choice!")
+ play_menu_sound()
+ return
+ play_menu_sound()
+ show_team_edit_radial(user, managed_teams[editing_team])
+
+/// Constantly shows a radial for editing a team until cancelled
+/obj/item/wargame_base_station/proc/show_team_edit_radial(mob/living/user, datum/wargaming_team/edited_team)
+ var/picked_choice = show_radial_menu(user, src, team_edit_radial_options, require_near = TRUE, tooltips = TRUE)
+ if(isnull(picked_choice))
+ return
+ switch(picked_choice)
+ if(BASESTATION_TEAM_NAME)
+ var/new_name = tgui_input_text(user, "Give your new team a name.", "Team Name", "Unnamed Combatants")
+ if(isnull(new_name))
+ balloon_alert(user, "need name!")
+ show_team_edit_radial(user, edited_team)
+ play_menu_sound()
+ return
+ if(new_name in managed_teams)
+ balloon_alert(user, "name in use!")
+ show_team_edit_radial(user, edited_team)
+ play_menu_sound()
+ return
+ play_menu_sound()
+ show_team_edit_radial(user, edited_team)
+ edited_team.team_name = new_name
+ if(BASESTATION_TEAM_COLOR)
+ var/new_color = input(user, "Choose your new team color." ,"Color Selection", COLOR_PRIDE_PURPLE) as color|null
+ if(isnull(new_color))
+ balloon_alert(user, "need color!")
+ show_team_edit_radial(user, edited_team)
+ play_menu_sound()
+ return
+ play_menu_sound()
+ show_team_edit_radial(user, edited_team)
+ edited_team.team_color = new_color
+ if(BASESTATION_TEAM_COMMANDER)
+ var/new_commander_title = tgui_input_text(user, "How will this team's units address the players? ALL LOWERCASE IF NOT PROPER!", "Commander Title", "fleet")
+ if(isnull(new_commander_title))
+ balloon_alert(user, "need title!")
+ show_team_edit_radial(user, edited_team)
+ play_menu_sound()
+ return
+ play_menu_sound()
+ edited_team.team_commander_reference = new_commander_title
+ show_team_edit_radial(user, edited_team)
+ if(BASESTATION_TEAM_PREFIX)
+ var/new_prefix = tgui_input_text(user, "Choose what your ships' automatic titles will be prefixed with, such as 4CA, TTV.", "Ship Name Prefix", "4CA")
+ if(isnull(new_prefix))
+ balloon_alert(user, "need prefix!")
+ show_team_edit_radial(user, edited_team)
+ play_menu_sound()
+ return
+ play_menu_sound()
+ edited_team.team_ship_prefix = new_prefix
+ show_team_edit_radial(user, edited_team)
+
+/// Lets players pick from a list of existing teams to delete one
+/obj/item/wargame_base_station/proc/delete_team(mob/living/user)
+ var/deleting_team = tgui_input_list(user, "Choose which team you wish to delete.", "Team List", managed_teams)
+ if(isnull(deleting_team))
+ balloon_alert(user, "no choice!")
+ play_menu_sound()
+ return
+ var/certainty = tgui_alert(user, "Are you certain you wish to delete [deleting_team]?", "Deletion Confirmation", list(MY_CHILD_WILL, MY_CHILD_WILL_NOT))
+ if(certainty == MY_CHILD_WILL)
+ qdel(managed_teams[deleting_team])
+ managed_teams -= deleting_team
+ play_menu_sound()
+
+/// Interactively create a new team with the user
+/obj/item/wargame_base_station/proc/create_new_team(mob/living/user)
+ var/datum/wargaming_team/new_team = new()
+ var/new_name = tgui_input_text(user, "Give your new team a name.", "Team Name", "Unnamed Combatants")
+ if(isnull(new_name)) // How?
+ qdel(new_team)
+ balloon_alert(user, "needs name!")
+ play_menu_sound()
+ return
+ if(new_name in managed_teams) // This name is already in use, get some new material
+ qdel(new_team)
+ balloon_alert(user, "name in use!")
+ play_menu_sound()
+ return
+ play_menu_sound()
+ new_team.team_name = new_name
+ var/new_color = input(user, "Choose your new team color." ,"Color Selection", COLOR_PRIDE_PURPLE) as color|null
+ if(isnull(new_color))
+ qdel(new_team)
+ balloon_alert(user, "needs color!")
+ play_menu_sound()
+ return
+ play_menu_sound()
+ new_team.team_color = new_color
+ var/new_commander_title = tgui_input_text(user, "How will this team's units address the players? ALL LOWERCASE IF NOT PROPER!", "Commander Title", "fleet")
+ if(isnull(new_commander_title))
+ balloon_alert(user, "need title!")
+ qdel(new_team)
+ play_menu_sound()
+ return
+ play_menu_sound()
+ new_team.team_commander_reference = new_commander_title
+ var/new_prefix = tgui_input_text(user, "Choose what your ships' automatic titles will be prefixed with, such as 4CA, TTV.", "Ship Name Prefix", "4CA")
+ if(isnull(new_prefix))
+ balloon_alert(user, "need prefix!")
+ qdel(new_team)
+ play_menu_sound()
+ return
+ play_menu_sound()
+ new_team.team_ship_prefix = new_prefix
+ managed_teams[new_team.team_name] += new_team
+
+/// Plays a clicking sound for menu actions
+/obj/item/wargame_base_station/proc/play_menu_sound()
+ playsound(src, SFX_REMOTE_MODE_SWITCH, 50, TRUE)
+
+#undef MY_CHILD_WILL
+#undef MY_CHILD_WILL_NOT
diff --git a/modular_doppler/wargaming/code/conditions/_unit_conditions.dm b/modular_doppler/wargaming/code/conditions/_unit_conditions.dm
new file mode 100644
index 00000000000000..d3ac1528d684ae
--- /dev/null
+++ b/modular_doppler/wargaming/code/conditions/_unit_conditions.dm
@@ -0,0 +1,63 @@
+/datum/wargame_condition
+ abstract_type = /datum/wargame_condition
+ /// The name of the condition
+ var/condition_name
+ /// Brief description of what the condition does
+ var/condition_desc
+ /// How many more turns until the condition goes away on its own
+ var/condition_lifetime_left
+
+/// When the condition is applied to a unit stats, what changes
+/datum/wargame_condition/proc/applied_to_unit(datum/wargame_unit_stats/stats, obj/hologram)
+ return
+
+/// When the condition is removed from a unit stats, what changes back
+/datum/wargame_condition/proc/removed_from_unit(datum/wargame_unit_stats/stats)
+ return
+
+// special condition for missiles just so they explode
+/datum/wargame_condition/missile_failure
+ condition_name = "Missile Failure"
+ condition_desc = "Complete failure of missile systems, safety self destruct imminent."
+ condition_lifetime_left = 2
+
+/datum/wargame_condition/missile_failure/applied_to_unit(datum/wargame_unit_stats/stats, obj/hologram)
+ stats.im_boutta_blow(hologram)
+
+/datum/wargame_condition/hull_damage
+ condition_name = "Hull Damage"
+ condition_desc = "Damage to the hull that reduces the armor class of the ship."
+ condition_lifetime_left = 2
+
+/datum/wargame_condition/hull_damage/applied_to_unit(datum/wargame_unit_stats/stats, obj/hologram)
+ stats.armor_class -= 1
+
+/datum/wargame_condition/hull_damage/removed_from_unit(datum/wargame_unit_stats/stats)
+ stats.armor_class += 1
+
+/datum/wargame_condition/hull_damage/asteroid
+ condition_name = "Surface Fracture"
+ condition_desc = "Fracturing of the asteroid surface that reduces the asteroid's armor class."
+ condition_lifetime_left = 10
+
+/datum/wargame_condition/engine_damage
+ condition_name = "Engine Damage"
+ condition_desc = "Damage to the engines that makes movement cost an extra action point."
+ condition_lifetime_left = 3
+
+/datum/wargame_condition/engine_damage/applied_to_unit(datum/wargame_unit_stats/stats, obj/hologram)
+ stats.movement_cost += 1
+
+/datum/wargame_condition/engine_damage/removed_from_unit(datum/wargame_unit_stats/stats)
+ stats.movement_cost -= 1
+
+/datum/wargame_condition/reactor_damage
+ condition_name = "Reactor Damage"
+ condition_desc = "Damage to the ship's reactor that reduces maximum action points by one."
+ condition_lifetime_left = 4
+
+/datum/wargame_condition/reactor_damage/applied_to_unit(datum/wargame_unit_stats/stats, obj/hologram)
+ stats.maximum_action_points -= 1
+
+/datum/wargame_condition/reactor_damage/removed_from_unit(datum/wargame_unit_stats/stats)
+ stats.maximum_action_points += 1
diff --git a/modular_doppler/wargaming/code/game_kit.dm b/modular_doppler/wargaming/code/game_kit.dm
index 729ee80fe39851..175b3e408f9bc4 100644
--- a/modular_doppler/wargaming/code/game_kit.dm
+++ b/modular_doppler/wargaming/code/game_kit.dm
@@ -1,29 +1,15 @@
/obj/item/storage/briefcase/secure/wargame_kit
- name = "DIY Wargaming Kit"
- desc = "Contains everything an aspiring naval officer (or just huge fucking nerd) would need for a proper modern naval wargame."
+ name = "\improper Portable Holographic Wargaming Kit"
+ desc = "A bulky kit containing a base station and five holographic projectors for all of your portable wargaming needs."
custom_premium_price = PAYCHECK_CREW * 2
/obj/item/storage/briefcase/secure/wargame_kit/PopulateContents()
var/static/items_inside = list(
+ /obj/item/wargame_base_station = 1,
/obj/item/wargame_projector/ships = 1,
/obj/item/wargame_projector/ships/red = 1,
+ /obj/item/wargame_projector/ships/yellow = 1,
+ /obj/item/wargame_projector/ships/green = 1,
/obj/item/wargame_projector/terrain = 1,
- /obj/item/storage/dice = 1,
- /obj/item/book/manual/wargame_rules = 1,
- /obj/item/book/manual/wargame_rules/examples = 1,
)
generate_items_inside(items_inside,src)
-
-/obj/item/book/manual/wargame_rules
- name = "Wargame: Blue Lizard - Example Ruleset"
- icon_state = "book"
- starting_author = "John War - CEO of War"
- starting_title = "Wargame: Blue Lizard - Example Ruleset"
- starting_content = "Formatting is a fuck - 2564 kill em all - Just go to this link in your browser https://hackmd.io/@Paxilmaniac/H1ZVsTIYR"
-
-/obj/item/book/manual/wargame_rules/examples
- name = "Wargame: Blue Lizard - Example Ships and Scenarios"
- icon_state = "book1"
- starting_author = "John War - CEO of War"
- starting_title = "Wargame: Blue Lizard - Example Ships and Scenarios"
- starting_content = "Formatting is a fuck - 2564 kill em all - Just go to this link in your browser https://hackmd.io/@Paxilmaniac/rJwy1C8KR"
diff --git a/modular_doppler/wargaming/code/holodeck.dm b/modular_doppler/wargaming/code/holodeck.dm
new file mode 100644
index 00000000000000..25b92cce7338ea
--- /dev/null
+++ b/modular_doppler/wargaming/code/holodeck.dm
@@ -0,0 +1,9 @@
+/turf/open/indestructible/wargame
+ icon = 'modular_doppler/wargaming/icons/turf.dmi'
+ icon_state = "holodeck"
+ holodeck_compatible = TRUE
+
+/datum/map_template/holodeck/doppler_wargame_board
+ name = "Holodeck - Wargame Simulator"
+ template_id = "holodeck_doppler_wargame_board"
+ mappath = "_maps/templates/holodeck_doppler_wargame_board.dmm"
diff --git a/modular_doppler/wargaming/code/holograms.dm b/modular_doppler/wargaming/code/holograms.dm
deleted file mode 100644
index 1d85021ef680c9..00000000000000
--- a/modular_doppler/wargaming/code/holograms.dm
+++ /dev/null
@@ -1,122 +0,0 @@
-/obj/structure/wargame_hologram
- name = "broken holographic wargame marker"
- desc = "You have a feeling like this is supposed to be telling you something, but the hologram must have broken."
- icon = 'modular_doppler/wargaming/icons/projectors_and_holograms.dmi'
- icon_state = null
- anchored = TRUE
- density = FALSE
- max_integrity = 1
- obj_flags = UNIQUE_RENAME
- /// What object created this projection? Can be null as a projector isn't required for this to exist
- var/obj/item/wargame_projector/projector
-
-/obj/structure/wargame_hologram/Initialize(mapload, source_projector)
- . = ..()
- if(source_projector)
- projector = source_projector
- LAZYADD(projector.projections, src)
-
-/obj/structure/wargame_hologram/Destroy()
- if(projector)
- LAZYREMOVE(projector.projections, src)
- projector = null
- return ..()
-
-/obj/structure/wargame_hologram/update_overlays()
- . = ..()
- . += emissive_appearance(icon, icon_state, src)
-
-/// Projections for 'moving vessels' in order from smallest to largest representation
-
-/obj/structure/wargame_hologram/strike_craft
- name = "strike craft marker"
- desc = "A hologram of a single strike craft."
- icon_state = "strikesingle"
-
-/obj/structure/wargame_hologram/strike_craft_util
- name = "skiff marker"
- desc = "A hologram of a single utility skiff."
- icon_state = "strike_utility"
-
-/obj/structure/wargame_hologram/strike_craft/wing
- name = "strike craft wing marker"
- desc = "A hologram of a wing of strike craft."
- icon_state = "strikewing"
-
-/obj/structure/wargame_hologram/ship_marker
- name = "small vessel marker"
- desc = "A hologram of a small frigate."
- icon_state = "smallship"
-
-/obj/structure/wargame_hologram/ship_marker/medium
- name = "medium vessel marker"
- desc = "A hologram of a destroyer."
- icon_state = "mediumship"
-
-/obj/structure/wargame_hologram/ship_marker/large
- name = "large vessel marker"
- desc = "A hologram of a large cruiser."
- icon_state = "bigship"
-
-/obj/structure/wargame_hologram/ship_marker/large/alternate
- name = "alternate large vessel marker"
- desc = "A hologram of a massive ship."
- icon_state = "bigship_alternate"
-
-/obj/structure/wargame_hologram/unidentified
- name = "unidentified contact marker"
- desc = "A hologram standing for an unidentified contact."
- icon_state = "unidentified"
-
-/*
-Projections for misc stuff, like stations, scout probes, or incoming missiles
-*/
-
-/obj/structure/wargame_hologram/missile_warning
- name = "in-flight missile marker"
- desc = "A hologram of a missile currently in flight."
- icon_state = "missile"
-
-/obj/structure/wargame_hologram/probe
- name = "probe marker"
- desc = "A hologram of a scout probe."
- icon_state = "probe"
-
-/obj/structure/wargame_hologram/stationary_structure
- name = "station marker"
- desc = "A hologram of a space station."
- icon_state = "station"
-
-/obj/structure/wargame_hologram/stationary_structure/platform
- name = "platform marker"
- desc = "A hologram of a small space platform."
- icon_state = "platform"
-
-/*
-Projections for space 'terrain' like asteroids and dust clouds
-*/
-
-/obj/structure/wargame_hologram/dust
- name = "dust field marker"
- desc = "A hologram of a field of stellar dust of some sort."
- icon_state = "dustcloud"
-
-/obj/structure/wargame_hologram/asteroid
- name = "small asteroid marker"
- desc = "A hologram of a small asteroid."
- icon_state = "asteroidsmall"
-
-/obj/structure/wargame_hologram/asteroid/large
- name = "large asteroid marker"
- desc = "A hologram of a large asteroid."
- icon_state = "asteroidlarge"
-
-/obj/structure/wargame_hologram/asteroid/cluster
- name = "asteroid cluster marker"
- desc = "A hologram of a cluster of asteroids."
- icon_state = "asteroidcluster"
-
-/obj/structure/wargame_hologram/planet
- name = "planetary body marker"
- desc = "A hologram of a planet."
- icon_state = "planet"
diff --git a/modular_doppler/wargaming/code/holograms/_holograms.dm b/modular_doppler/wargaming/code/holograms/_holograms.dm
new file mode 100644
index 00000000000000..c8900a75c300b4
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/_holograms.dm
@@ -0,0 +1,120 @@
+/obj/structure/wargame_hologram
+ abstract_type = /obj/structure/wargame_hologram
+ icon = 'modular_doppler/wargaming/icons/holograms.dmi'
+ icon_state = null
+ anchored = TRUE
+ density = FALSE
+ max_integrity = 30
+ alpha = 180
+ obj_flags = UNIQUE_RENAME
+ do_not_override_chat_colors = TRUE
+ /// What object created this projection? Can be null as a projector isn't required for this to exist
+ var/obj/item/wargame_projector/projector
+ /// If this hologram ignores pixel shifting when placed, instead using swarming
+ var/swarming = FALSE
+ /// The team datum responsible for managing this hologram, if any
+ var/datum/weakref/team_reference
+ /// Does this controllable thing require a mob interacting with it to be on its team
+ var/requires_same_team = TRUE
+ /// Is this hologram controllable in any way, false for things like dust clouds and asteroids
+ var/controllable = FALSE
+ /// This object's unit stats datum, to be created on init
+ var/datum/wargame_unit_stats/unit_stats = /datum/wargame_unit_stats/generic
+
+/obj/structure/wargame_hologram/Initialize(mapload, source_projector)
+ . = ..()
+ unit_stats = new unit_stats()
+ unit_stats.set_hologram_name(src)
+ if(source_projector)
+ projector = source_projector
+ LAZYADD(projector.projections, src)
+ if(swarming)
+ AddComponent(/datum/component/swarming, max_x = 12, max_y = 12)
+ layer = LOW_ITEM_LAYER
+ register_context()
+
+/obj/structure/wargame_hologram/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
+ if(istype(held_item, /obj/item/wargame_projector))
+ context[SCREENTIP_CONTEXT_LMB] = "Delete hologram"
+ else
+ context[SCREENTIP_CONTEXT_LMB] = "Open actions menu"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/structure/wargame_hologram/examine(mob/user)
+ . = ..()
+ var/datum/wargaming_team/our_team = team_reference?.resolve()
+ if(!isnull(our_team))
+ . += span_notice("It belongs to the [our_team.team_name] team.")
+ if(controllable)
+ . += "This unit has [unit_stats.action_points] / [unit_stats.maximum_action_points] action points."
+ . += "It costs this unit [unit_stats.movement_cost] action points to move."
+ if(length(unit_stats.current_conditions))
+ var/conditions_text = ""
+ for(var/datum/wargame_condition/condition as anything in unit_stats.current_conditions)
+ conditions_text += "[span_boldnotice("[condition.condition_name]")] - [condition.condition_desc] - [condition.condition_lifetime_left] turns until cleared.
"
+ . += fieldset_block(span_bold("Conditions"), conditions_text, "boxed_message")
+ if(length(unit_stats.current_conditions) > unit_stats.conditions_limit)
+ . += span_warning("It looks like it's nearing its limit, if these conditions don't improve by next effects phase, the ship will be lost.")
+ if(length(unit_stats.weaponry))
+ var/weaponry_text = ""
+ for(var/datum/wargame_weapon/weapon as anything in unit_stats.weaponry)
+ weaponry_text += "[span_boldnotice("[weapon.weapon_name]")] - [weapon.weapon_description()][!isnull(weapon.maximum_ammo) ? " [weapon.maximum_ammo] shots left" : ""]
"
+ . += fieldset_block(span_bold("Weaponry"), weaponry_text, "boxed_message")
+
+/obj/structure/wargame_hologram/Destroy()
+ if(projector)
+ LAZYREMOVE(projector.projections, src)
+ projector = null
+ return ..()
+
+/obj/structure/wargame_hologram/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, icon_state, src)
+
+/obj/structure/wargame_hologram/attack_hand(mob/living/user, list/modifiers)
+ if(user.combat_mode || !controllable)
+ return ..()
+ hologram_controls(user)
+
+/// Handles controlling the hologram when clicked on
+/obj/structure/wargame_hologram/proc/hologram_controls(mob/living/user)
+ SHOULD_CALL_PARENT(TRUE)
+ if(!currently_our_turn())
+ if(prob(1/1000))
+ say("WAIT. YOUR. TURN!")
+ playsound(src, 'sound/effects/seedling_chargeup.ogg', 50)
+ else
+ balloon_alert(user, "not our turn!")
+ return
+ if(requires_same_team && !verify_user_team(user))
+ balloon_alert(user, "wrong team!")
+ return
+ unit_stats.basic_actions(user, src)
+
+/// Checks with our projector's linked controller to see if it's our turn to move
+/obj/structure/wargame_hologram/proc/currently_our_turn()
+ var/obj/item/wargame_base_station/base_station = projector.linked_base_station?.resolve()
+ if(isnull(base_station))
+ return FALSE
+ var/datum/wargaming_team/our_team = team_reference?.resolve()
+ if(isnull(our_team))
+ return FALSE
+ if(base_station.turn_mode == WARGAME_TURN_MODE_SIMULTANEOUS)
+ return TRUE
+ if(base_station.team_turn != our_team)
+ return FALSE
+ return TRUE
+
+/// Checks if the user is in the team datum's players list
+/obj/structure/wargame_hologram/proc/verify_user_team(mob/living/user)
+ var/datum/wargaming_team/owner_team = team_reference?.resolve()
+ if(isnull(owner_team))
+ return FALSE
+ if(user in owner_team.team_players)
+ return TRUE
+ return FALSE
+
+/obj/structure/wargame_hologram/controllable
+ abstract_type = /obj/structure/wargame_hologram/controllable
+ swarming = TRUE
+ controllable = TRUE
diff --git a/modular_doppler/wargaming/code/holograms/asteroid.dm b/modular_doppler/wargaming/code/holograms/asteroid.dm
new file mode 100644
index 00000000000000..3a9f599fb40c83
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/asteroid.dm
@@ -0,0 +1,17 @@
+/obj/structure/wargame_hologram/asteroid
+ name = "asteroid"
+ icon_state = "asteroidsmall"
+ unit_stats = /datum/wargame_unit_stats/terrain/asteroid
+
+/obj/structure/wargame_hologram/asteroid/large
+ name = "large asteroid"
+ icon_state = "asteroidlarge"
+
+/obj/structure/wargame_hologram/asteroid/shattered
+ name = "asteroid cluster"
+ icon_state = "asteroidcluster"
+
+/obj/structure/wargame_hologram/dust
+ name = /datum/wargame_unit_stats/terrain/dust_cloud::unit_class
+ icon_state = "dustcloud"
+ unit_stats = /datum/wargame_unit_stats/terrain/dust_cloud
diff --git a/modular_doppler/wargaming/code/holograms/corvette.dm b/modular_doppler/wargaming/code/holograms/corvette.dm
new file mode 100644
index 00000000000000..525f7955da505b
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/corvette.dm
@@ -0,0 +1,4 @@
+/obj/structure/wargame_hologram/controllable/picket_corvette
+ name = /datum/wargame_unit_stats/corvette/picket::unit_class
+ icon_state = "small_ship"
+ unit_stats = /datum/wargame_unit_stats/corvette/picket
diff --git a/modular_doppler/wargaming/code/holograms/cruiser.dm b/modular_doppler/wargaming/code/holograms/cruiser.dm
new file mode 100644
index 00000000000000..fbfad9de3ae58c
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/cruiser.dm
@@ -0,0 +1,14 @@
+/obj/structure/wargame_hologram/controllable/mass_driver_cruiser
+ name = /datum/wargame_unit_stats/cruiser/mass_driver::unit_class
+ icon_state = "large_ship"
+ unit_stats = /datum/wargame_unit_stats/cruiser/mass_driver
+
+/obj/structure/wargame_hologram/controllable/artillery_cruiser
+ name = /datum/wargame_unit_stats/cruiser/artillery::unit_class
+ icon_state = "large_ship"
+ unit_stats = /datum/wargame_unit_stats/cruiser/artillery
+
+/obj/structure/wargame_hologram/controllable/battlecruiser
+ name = /datum/wargame_unit_stats/cruiser/linebreaker::unit_class
+ icon_state = "large_ship_alt"
+ unit_stats = /datum/wargame_unit_stats/cruiser/linebreaker
diff --git a/modular_doppler/wargaming/code/holograms/destroyer.dm b/modular_doppler/wargaming/code/holograms/destroyer.dm
new file mode 100644
index 00000000000000..6f41e6b013113d
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/destroyer.dm
@@ -0,0 +1,14 @@
+/obj/structure/wargame_hologram/controllable/destroyer
+ name = /datum/wargame_unit_stats/destroyer/brawler::unit_class
+ icon_state = "med_ship"
+ unit_stats = /datum/wargame_unit_stats/destroyer/brawler
+
+/obj/structure/wargame_hologram/controllable/beam_frigate
+ name = /datum/wargame_unit_stats/destroyer/beam::unit_class
+ icon_state = "med_ship"
+ unit_stats = /datum/wargame_unit_stats/destroyer/beam
+
+/obj/structure/wargame_hologram/controllable/missile_frigate
+ name = /datum/wargame_unit_stats/destroyer/missile::unit_class
+ icon_state = "med_ship"
+ unit_stats = /datum/wargame_unit_stats/destroyer/missile
diff --git a/modular_doppler/wargaming/code/holograms/missile.dm b/modular_doppler/wargaming/code/holograms/missile.dm
new file mode 100644
index 00000000000000..7aa02ac54f4f36
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/missile.dm
@@ -0,0 +1,11 @@
+/obj/structure/wargame_hologram/controllable/cruise_missile
+ icon_state = "missile"
+ unit_stats = /datum/wargame_unit_stats/missile/cruise
+
+/obj/structure/wargame_hologram/controllable/swarm_missile
+ icon_state = "missile"
+ unit_stats = /datum/wargame_unit_stats/missile/swarm
+
+/obj/structure/wargame_hologram/controllable/torpedo
+ icon_state = "missile"
+ unit_stats = /datum/wargame_unit_stats/missile/torpedo
diff --git a/modular_doppler/wargaming/code/holograms/static.dm b/modular_doppler/wargaming/code/holograms/static.dm
new file mode 100644
index 00000000000000..0dd0dd682bf5bd
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/static.dm
@@ -0,0 +1,17 @@
+/obj/structure/wargame_hologram/controllable/platform
+ swarming = FALSE
+ name = /datum/wargame_unit_stats/platform/outpost::unit_class
+ icon_state = "platform"
+ unit_stats = /datum/wargame_unit_stats/platform/outpost
+
+/obj/structure/wargame_hologram/controllable/civilian_station
+ swarming = FALSE
+ name = /datum/wargame_unit_stats/platform/civilian::unit_class
+ icon_state = "station"
+ unit_stats = /datum/wargame_unit_stats/platform/civilian
+
+/obj/structure/wargame_hologram/controllable/military_station
+ swarming = FALSE
+ name = /datum/wargame_unit_stats/platform/citadel::unit_class
+ icon_state = "station"
+ unit_stats = /datum/wargame_unit_stats/platform/citadel
diff --git a/modular_doppler/wargaming/code/holograms/strike.dm b/modular_doppler/wargaming/code/holograms/strike.dm
new file mode 100644
index 00000000000000..caa92b6a05ce1c
--- /dev/null
+++ b/modular_doppler/wargaming/code/holograms/strike.dm
@@ -0,0 +1,9 @@
+/obj/structure/wargame_hologram/controllable/strike_wing
+ name = /datum/wargame_unit_stats/strike/wing::unit_class
+ icon_state = "strike_wing"
+ unit_stats = /datum/wargame_unit_stats/strike/wing
+
+/obj/structure/wargame_hologram/controllable/interceptor_wing
+ name = /datum/wargame_unit_stats/strike/interceptor::unit_class
+ icon_state = "strike"
+ unit_stats = /datum/wargame_unit_stats/strike/interceptor
diff --git a/modular_doppler/wargaming/code/projectors.dm b/modular_doppler/wargaming/code/projectors.dm
index 18729d3d1913a7..fa6cc6496d6939 100644
--- a/modular_doppler/wargaming/code/projectors.dm
+++ b/modular_doppler/wargaming/code/projectors.dm
@@ -1,23 +1,21 @@
/obj/item/wargame_projector
- name = "holographic projector"
- desc = "A handy-dandy holographic projector developed by the Port Authority Naval Command for playing wargames with, this one seems broken."
- icon = 'modular_doppler/wargaming/icons/projectors_and_holograms.dmi'
+ name = "holographic wargame projector"
+ desc = "A holographic projectors for creating holograms that work in the wargaming system."
+ icon = 'modular_doppler/wargaming/icons/items.dmi'
icon_state = "projector"
base_icon_state = "projector"
- inhand_icon_state = "electronic"
- worn_icon_state = "electronic"
- lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
- force = 0
+ lefthand_file = 'modular_doppler/wargaming/icons/mob/lefthand.dmi'
+ righthand_file = 'modular_doppler/wargaming/icons/mob/righthand.dmi'
+ inhand_icon_state = "generic"
+ worn_icon = 'modular_doppler/wargaming/icons/mob/worn.dmi'
+ worn_icon_state = "generic"
+ slot_flags = ITEM_SLOT_BELT
w_class = WEIGHT_CLASS_SMALL
- throwforce = 0
- throw_speed = 3
- throw_range = 7
item_flags = NOBLUDGEON
+ pickup_sound = SFX_GENERIC_DEVICE_PICKUP
+ drop_sound = SFX_GENERIC_DEVICE_DROP
/// All of the signs this projector is maintaining
var/list/projections
- /// The maximum number of projections this can support
- var/max_signs = 1
/// The color to give holograms when created
var/holosign_color = COLOR_WHITE
/// The type of hologram to spawn on click
@@ -44,30 +42,46 @@
/// A names to path list for the projections filled out by populate_radial_choice_lists() on init
var/list/projection_names_to_path = list()
+ /// Weakref to a linked base station, required for placing holograms
+ var/datum/weakref/linked_base_station
+ /// If this projector requires a linked team in order to place holograms as well
+ var/requires_linked_team = FALSE
+ /// Weakref to a linked team, for drawing color from
+ var/datum/weakref/linked_team
+
/obj/item/wargame_projector/Initialize(mapload)
. = ..()
- AddElement(/datum/element/openspace_item_click_handler)
update_appearance()
populate_radial_choice_lists()
+ register_context()
-/obj/item/wargame_projector/handle_openspace_click(turf/target, mob/user, click_parameters)
- ranged_interact_with_atom(target, user, params2list(click_parameters))
+/obj/item/wargame_projector/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
+ context[SCREENTIP_CONTEXT_LMB] = "Change hologram"
+ if(!requires_linked_team)
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Change hologram color"
+ context[SCREENTIP_CONTEXT_CTRL_LMB] = "Clear all holograms"
+ return CONTEXTUAL_SCREENTIP_SET
-/obj/item/wargame_projector/update_appearance()
+/obj/item/wargame_projector/update_overlays()
. = ..()
- cut_overlays()
-
var/image/color_select_overlay = image(icon = icon, icon_state = "[base_icon_state]_screen")
color_select_overlay.color = holosign_color
- add_overlay(color_select_overlay)
+ . += color_select_overlay
+ . += emissive_appearance(icon, "[base_icon_state]_screen", src)
/obj/item/wargame_projector/examine(mob/user)
. = ..()
- if(projections)
- . += span_notice("It is currently maintaining [projections.len]/[max_signs] projections.")
- . += span_notice("Use the projector in hand to change what type of hologram it creates.")
- . += span_notice("Alt clicking the projector will let you change the color of the next hologram it makes.")
- . += span_warning("Control clicking the projector will allow you to clear all active holograms.")
+ var/datum/wargaming_team/our_team = linked_team?.resolve()
+ if(!isnull(our_team))
+ . += span_notice("This projector is linked to the [our_team.team_name] team!")
+ . += "Use the projector [EXAMINE_HINT("in hand")] to change what type of hologram it creates."
+ if(!requires_linked_team)
+ . += "[EXAMINE_HINT("Alt-click")] will let you change the color of the next hologram it makes."
+ . += span_warning("[EXAMINE_HINT("Ctrl-Click")] will allow you to clear all active holograms.")
+
+/// Plays a clicking sound for menu actions
+/obj/item/wargame_projector/proc/play_menu_sound()
+ playsound(src, SFX_REMOTE_ACTION, 50, TRUE)
/obj/item/wargame_projector/proc/populate_radial_choice_lists()
if(!length(radial_choices) || !length(projection_names_to_path))
@@ -77,6 +91,7 @@
/// Changes the selected hologram to one of the options from the hologram list
/obj/item/wargame_projector/proc/select_hologram(mob/user)
+ play_menu_sound()
var/picked_choice = show_radial_menu(
user,
src,
@@ -84,23 +99,26 @@
require_near = TRUE,
tooltips = TRUE,
)
-
if(isnull(picked_choice))
return
-
holosign_type = projection_names_to_path[picked_choice]
+ play_menu_sound()
/obj/item/wargame_projector/attack_self(mob/user)
select_hologram(user)
/obj/item/wargame_projector/click_alt(mob/user)
+ if(requires_linked_team)
+ return ..() // Controllers linked to a team are locked to that team's color
var/selected_color = tgui_input_list(user, "Select a color", "Color Selection", color_options)
if(isnull(selected_color))
balloon_alert(user, "no color change")
+ play_menu_sound()
return
var/color_to_set_to = color_options[selected_color]
holosign_color = color_to_set_to
balloon_alert(user, "color changed")
+ play_menu_sound()
update_appearance()
return CLICK_ACTION_SUCCESS
@@ -108,92 +126,136 @@
if(tgui_alert(usr,"Clear all currently active holograms?", "Hologram Removal", list("Yes", "No")) == "Yes")
for(var/hologram as anything in projections)
qdel(hologram)
+ play_menu_sound()
return CLICK_ACTION_SUCCESS
/// Can we place a hologram at the target location?
-/obj/item/wargame_projector/proc/check_can_place_hologram(atom/target, mob/user, team)
+/obj/item/wargame_projector/proc/check_can_place_hologram(atom/target, mob/user)
+ var/obj/item/wargame_base_station/base_station = linked_base_station?.resolve()
+ if(isnull(base_station))
+ user.balloon_alert(user, "no basestation!")
+ return FALSE
+ if(base_station.game_phase != WARGAME_PHASE_PLACEMENT)
+ user.balloon_alert(user, "wrong game phase!")
+ return FALSE
if(!check_allowed_items(target, not_inside = TRUE))
return FALSE
var/turf/target_turf = get_turf(target)
if(target_turf.is_blocked_turf(TRUE))
return FALSE
- if(LAZYLEN(projections) >= max_signs)
- balloon_alert(user, "max capacity!")
- return FALSE
return TRUE
/// Spawn a hologram with pixel offset based on where the user clicked
/obj/item/wargame_projector/proc/create_hologram(atom/target, mob/user, list/modifiers)
- var/obj/target_holosign = new holosign_type(get_turf(target), src)
-
+ var/obj/structure/wargame_hologram/target_holosign = new holosign_type(get_turf(target), src)
+ target_holosign.color = holosign_color
+ target_holosign.chat_color = holosign_color
+ target_holosign.update_appearance(UPDATE_OVERLAYS)
+ playsound(loc, SFX_INDUSTRIAL_SCAN, 45, TRUE)
+ if(requires_linked_team)
+ target_holosign.team_reference = linked_team
+ target_holosign.unit_stats.set_hologram_name(target_holosign)
+ if(target_holosign.swarming)
+ return // Ships use the swarming component and don't get specific pixel offsets
var/click_x
var/click_y
-
if(LAZYACCESS(modifiers, ICON_X) && LAZYACCESS(modifiers, ICON_Y))
click_x = clamp(text2num(LAZYACCESS(modifiers, ICON_X)) - 16, -(world.icon_size/2), world.icon_size/2)
click_y = clamp(text2num(LAZYACCESS(modifiers, ICON_Y)) - 16, -(world.icon_size/2), world.icon_size/2)
-
target_holosign.pixel_x = click_x
target_holosign.pixel_y = click_y
- target_holosign.color = holosign_color
-
- playsound(loc, 'sound/machines/click.ogg', 20, TRUE)
-
/obj/item/wargame_projector/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
+ if(istype(interacting_with, /obj/item/wargame_base_station))
+ var/obj/item/wargame_base_station/base_station = interacting_with
+ balloon_alert(user, "linking...")
+ var/datum/wargaming_team/reference_team
+ if(requires_linked_team)
+ var/picked_team = tgui_input_list(user, "Pick a team to link to.", "Team Picker", base_station.managed_teams)
+ if(isnull(picked_team))
+ base_station.balloon_alert(user, "needs team!")
+ play_menu_sound()
+ return NONE
+ reference_team = base_station.managed_teams[picked_team]
+ reference_team.tracked_projectors += src
+ linked_team = WEAKREF(reference_team)
+ else
+ base_station.terrain_projectors += src
+ if(!do_after(user, 3 SECONDS, base_station))
+ play_menu_sound()
+ return NONE
+ if(!isnull(reference_team))
+ holosign_color = reference_team.team_color
+ update_appearance()
+ playsound(src, SFX_INDUSTRIAL_SCAN, 50, TRUE)
+ linked_base_station = WEAKREF(base_station)
+ return ITEM_INTERACT_SUCCESS
if(istype(interacting_with, /obj/structure/wargame_hologram))
+ var/obj/item/wargame_base_station/base_station = linked_base_station?.resolve()
+ if(!isnull(base_station))
+ if(base_station.game_phase != WARGAME_PHASE_PLACEMENT)
+ user.balloon_alert(user, "wrong game phase!")
+ play_menu_sound()
+ return NONE
qdel(interacting_with)
+ play_menu_sound()
return ITEM_INTERACT_SUCCESS
- if(!check_can_place_hologram(interacting_with, user, 1))
+ if(!isturf(interacting_with))
+ return ..()
+ if(!check_can_place_hologram(interacting_with, user))
+ play_menu_sound()
return NONE
create_hologram(interacting_with, user, modifiers)
return ITEM_INTERACT_SUCCESS
/obj/item/wargame_projector/Destroy()
QDEL_LAZYLIST(projections)
+ var/datum/wargaming_team/our_team = linked_team?.resolve()
+ if(!isnull(our_team))
+ our_team.tracked_projectors -= src
. = ..()
/// Actual projector types, split between the 'categories' of things they can project
/obj/item/wargame_projector/ships
name = "holographic unit projector"
- desc = "A handy-dandy holographic projector developed by the Port Authority Naval Command for playing wargames with, this one creates markers for 'units'."
- max_signs = 30
+ desc = "A holographic projectors for creating holograms that work in the wargaming system. Must be linked to a base station and team before use."
holosign_color = COLOR_BLUE_LIGHT
- holosign_type = /obj/structure/wargame_hologram/ship_marker
+ holosign_type = /obj/structure/wargame_hologram/controllable/strike_wing
holosign_options = list(
- /obj/structure/wargame_hologram/unidentified,
- /obj/structure/wargame_hologram/missile_warning,
- /obj/structure/wargame_hologram/strike_craft,
- /obj/structure/wargame_hologram/strike_craft_util,
- /obj/structure/wargame_hologram/strike_craft/wing,
- /obj/structure/wargame_hologram/ship_marker,
- /obj/structure/wargame_hologram/ship_marker/medium,
- /obj/structure/wargame_hologram/ship_marker/large,
- /obj/structure/wargame_hologram/ship_marker/large/alternate,
- /obj/structure/wargame_hologram/probe,
- /obj/structure/wargame_hologram/stationary_structure,
- /obj/structure/wargame_hologram/stationary_structure/platform,
+ /obj/structure/wargame_hologram/controllable/strike_wing,
+ /obj/structure/wargame_hologram/controllable/interceptor_wing,
+ /obj/structure/wargame_hologram/controllable/picket_corvette,
+ /obj/structure/wargame_hologram/controllable/missile_frigate,
+ /obj/structure/wargame_hologram/controllable/beam_frigate,
+ /obj/structure/wargame_hologram/controllable/destroyer,
+ /obj/structure/wargame_hologram/controllable/mass_driver_cruiser,
+ /obj/structure/wargame_hologram/controllable/artillery_cruiser,
+ /obj/structure/wargame_hologram/controllable/battlecruiser,
+ /obj/structure/wargame_hologram/controllable/platform,
+ /obj/structure/wargame_hologram/controllable/civilian_station,
+ /obj/structure/wargame_hologram/controllable/military_station,
)
+ requires_linked_team = TRUE
/obj/item/wargame_projector/ships/red
holosign_color = COLOR_RED_LIGHT
+/obj/item/wargame_projector/ships/yellow
+ holosign_color = COLOR_VIVID_YELLOW
+
+/obj/item/wargame_projector/ships/green
+ holosign_color = COLOR_VIBRANT_LIME
+
/obj/item/wargame_projector/terrain
name = "holographic terrain projector"
desc = "A handy-dandy holographic projector developed by the Port Authority Naval Command for playing wargames with, this one creates markers for space 'terrain'."
- max_signs = 30
holosign_color = COLOR_GRAY
holosign_type = /obj/structure/wargame_hologram/asteroid
// Some things, like stations, probes, and unidentified contacts, can be in the terrain one just because I can see situations where that's desired
holosign_options = list(
- /obj/structure/wargame_hologram/unidentified,
- /obj/structure/wargame_hologram/dust,
/obj/structure/wargame_hologram/asteroid,
/obj/structure/wargame_hologram/asteroid/large,
- /obj/structure/wargame_hologram/asteroid/cluster,
- /obj/structure/wargame_hologram/planet,
- /obj/structure/wargame_hologram/probe,
- /obj/structure/wargame_hologram/stationary_structure,
- /obj/structure/wargame_hologram/stationary_structure/platform,
+ /obj/structure/wargame_hologram/asteroid/shattered,
+ /obj/structure/wargame_hologram/dust,
)
diff --git a/modular_doppler/wargaming/code/team_datums.dm b/modular_doppler/wargaming/code/team_datums.dm
new file mode 100644
index 00000000000000..13dac1ea2c04b5
--- /dev/null
+++ b/modular_doppler/wargaming/code/team_datums.dm
@@ -0,0 +1,28 @@
+/datum/wargaming_team
+ abstract_type = /datum/wargaming_team
+ /// The name of the team as displayed on the controllers
+ var/team_name
+ /// The color of the team as shown in holograms and outfits
+ var/team_color = "#fff"
+ /// How does this team's units reference their commander(s)
+ var/team_commander_reference = "fleet"
+ /// The prefix applied to all automatically generated ship names for this team
+ var/team_ship_prefix = "4CA"
+ /// The players currently on this team
+ var/list/team_players = list()
+ /// Tracks hologram projectors associated with this team for updating unit action points with
+ var/list/tracked_projectors = list()
+
+/// Sets every controllable hologram in our tracked controllers back to its default amount of action points
+/datum/wargaming_team/proc/ready_all_units()
+ for(var/obj/item/wargame_projector/projector as anything in tracked_projectors)
+ for(var/obj/structure/wargame_hologram/hologram as anything in projector.projections)
+ if(hologram.controllable)
+ hologram.unit_stats.make_ready()
+
+/// Tells every controllable hologram to update its conditions and explode if necessary
+/datum/wargaming_team/proc/update_all_units()
+ for(var/obj/item/wargame_projector/projector as anything in tracked_projectors)
+ for(var/obj/structure/wargame_hologram/hologram as anything in projector.projections)
+ if(hologram.controllable)
+ hologram.unit_stats.effects_phase_process(hologram)
diff --git a/modular_doppler/wargaming/code/unit_stats/_unit_stats.dm b/modular_doppler/wargaming/code/unit_stats/_unit_stats.dm
new file mode 100644
index 00000000000000..b31b4d684f01f7
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/_unit_stats.dm
@@ -0,0 +1,324 @@
+/datum/wargame_unit_stats
+ abstract_type = /datum/wargame_unit_stats
+ /// The class of unit, "missile destroyer" or "asteroid" as examples
+ var/unit_class
+ /// Does this unit generate a random name for itself? Use only on ships
+ var/generates_name = FALSE
+ /// This unit's name, automatically generated or set manually. Can be null for no special name
+ var/unit_name
+ /// This unit's description
+ var/unit_description
+ /// How many conditions this unit can sustain before it is destroyed
+ var/conditions_limit = 3
+ /// A list of conditions currently applied on this unit
+ var/list/current_conditions = list()
+ /// List of all possible conditions that can be applied to this unit
+ var/list/possible_conditions = list()
+ /// This unit's armor class, or the amount an attack needs to roll above in order to cause a condition
+ var/armor_class = 10
+ /// This unit's evasion, added to the armor class for applicable attacks such as gunfire
+ var/evasion_modifier = 0
+ /// This unit's movement cost, how many action points to move one tile
+ var/movement_cost = 1
+ /// This unit's maximum action points per turn
+ var/maximum_action_points = 3
+ /// This unit's current action points
+ var/action_points = 0
+ /// If this unit counts as a "small vessel" for the purpose of attacks
+ var/is_small_vessel = FALSE
+ /// If this unit can be targetted by others at all, use for background stuff like space dust
+ var/can_be_a_target = TRUE
+ /// How big this unit is compared to others, used for cover calculations
+ var/unit_size = WARGAME_SIZE_SMALL
+ /// If this unit is talkative and will speak voicelines during specific actions
+ var/talkative = TRUE
+ /// How does this unit refer to the players in charge of it? Set by the team datum
+ var/commander = "fleet"
+ /// Associative list of weapon to radial choice, should be a list of weapons on init
+ var/list/weaponry = list()
+
+/datum/wargame_unit_stats/New()
+ set_up_weaponry()
+
+/// Sets the name of the attached hologram
+/datum/wargame_unit_stats/proc/set_hologram_name(obj/structure/wargame_hologram/hologram)
+ var/datum/wargaming_team/team = hologram.team_reference?.resolve()
+ if(!isnull(team))
+ commander = team.team_commander_reference
+ if(!isnull(unit_description))
+ hologram.desc = unit_description
+ if(isnull(unit_class) && isnull(unit_name))
+ return
+ if(generates_name)
+ unit_name = create_unit_name(team)
+ if(isnull(unit_name))
+ hologram.name = unit_class
+ else
+ hologram.name = "[unit_class] - ([unit_name])"
+
+/// Generates a name for the unit, overwriten by subtypes for different naming schemes
+/datum/wargame_unit_stats/proc/create_unit_name(datum/wargaming_team/team)
+ if(isnull(team))
+ return pick_list_replacements("~doppler/salvage_shuttle.json", "ship_name")
+ else
+ return "[team.team_ship_prefix] [pick_list_replacements("~doppler/salvage_shuttle.json", "ship_name_no_designation")]"
+
+/// Sets up weapon datums and radial options
+/datum/wargame_unit_stats/proc/set_up_weaponry()
+ if(!length(weaponry))
+ return // We just don't have weapons
+ var/list/temporary_weapons = list()
+ for(var/datum/wargame_weapon/weapon as anything in weaponry)
+ weapon = new weapon()
+ var/datum/radial_menu_choice/choice = new()
+ choice.name = weapon.weapon_name
+ choice.image = image(icon = WARGAME_ACTIONS_FILE, icon_state = weapon.radial_icon_state)
+ choice.info = weapon.weapon_description()
+ temporary_weapons[weapon] = choice
+ weaponry = temporary_weapons
+
+/// Sets our action points back to max
+/datum/wargame_unit_stats/proc/make_ready()
+ action_points = maximum_action_points
+
+/// Runs through everything we might need to process during the effects phase
+/datum/wargame_unit_stats/proc/effects_phase_process(obj/structure/wargame_hologram/hologram)
+ var/repaired_this_phase = FALSE
+ for(var/datum/wargame_condition/condition as anything in current_conditions)
+ condition.condition_lifetime_left--
+ if(condition.condition_lifetime_left <= 0)
+ condition.removed_from_unit(src)
+ current_conditions -= condition
+ repaired_this_phase = TRUE
+ qdel(condition)
+ if(length(current_conditions) > conditions_limit)
+ im_boutta_blow(hologram)
+ return
+ if(repaired_this_phase)
+ var/list/lines = list(
+ "Conditions repaired [commander], combat functionality restored.",
+ "Repairs complete [commander], let's get back in the fight!",
+ "[capitalize(commander)]? Damage control reports successful repairs.",
+ "One less hole in the wall [commander], let's keep at it!",
+ "Integrity partially restored [commander], let's show them we're still in this!",
+ )
+ hologram.say(pick(lines))
+ playsound(hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+
+/// What to do when this unit explodes, good place to spawn a replacement "wreck" unit type
+/datum/wargame_unit_stats/proc/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ if(talkative)
+ var/list/lines = list(
+ "Reactor critical, all crew punch out!",
+ "We're losing her!",
+ "It's not- It's not shutting down!",
+ "Damage critical, all crew to lifeboats.",
+ "Red alert! Abandon ship!",
+ "Send for rescue [commander]! We're-",
+ "Punch out! Punch out! We're had!",
+ )
+ hologram.say(pick(lines))
+ playsound(hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+ playsound(hologram, 'modular_doppler/wargaming/sound/ship_explode.ogg', 50, TRUE)
+ do_sparks(3, FALSE, hologram)
+ qdel(hologram)
+
+/// Shows the menu for basic actions before moving into details
+/datum/wargame_unit_stats/proc/basic_actions(mob/living/user, obj/hologram)
+ var/list/basic_actions_radial = get_unit_basic_actions()
+ var/radial_choice = show_radial_menu(user, hologram, basic_actions_radial)
+ if(isnull(radial_choice))
+ return
+ perform_basic_actions(radial_choice, user, hologram)
+
+/// Returns a list of basic actions this unit can perform
+/datum/wargame_unit_stats/proc/get_unit_basic_actions()
+ var/static/our_actions = list(
+ WARGAME_UNIT_MOVE = image(icon = WARGAME_ACTIONS_FILE, icon_state = "unit_move"),
+ WARGAME_UNIT_ATTACK = image(icon = WARGAME_ACTIONS_FILE, icon_state = "unit_attack"),
+ // WARGAME_UNIT_SPECIAL = image(icon = WARGAME_ACTIONS_FILE, icon_state = "unit_special"), // To come later
+ )
+ return our_actions
+
+/// Breaks out into our different procs for actions based on what was chosen
+/datum/wargame_unit_stats/proc/perform_basic_actions(radial_choice, mob/living/user, obj/hologram)
+ if(isnull(radial_choice))
+ return
+ switch(radial_choice)
+ if(WARGAME_UNIT_MOVE)
+ unit_movement(user, hologram)
+ if(WARGAME_UNIT_ATTACK)
+ unit_attacks(user, hologram)
+
+/// Brings up a list of weapons to try and attack targets with
+/datum/wargame_unit_stats/proc/unit_attacks(mob/living/user, obj/hologram)
+ var/datum/wargame_weapon/weapon_choice = show_radial_menu(user, hologram, weaponry)
+ if(isnull(weapon_choice))
+ return
+ var/obj/structure/wargame_hologram/target_hologram = weapon_choice.pick_target(user, hologram)
+ if(isnull(target_hologram))
+ return
+ if(!weapon_choice.prefire_checks(user, src, hologram, target_hologram))
+ return
+ if(weapon_choice.firing_voiceline(src) && talkative)
+ hologram.say(weapon_choice.firing_voiceline(src))
+ playsound(hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+ weapon_choice.weapon_firing_message(hologram, target_hologram)
+ weapon_choice.weapon_firing_sound(hologram)
+ if(!weapon_choice.all_special_effects)
+ var/impact = target_hologram.unit_stats.get_attacked(user, hologram, target_hologram, weapon_choice)
+ weapon_choice.special_effects_fire(user, src, hologram, target_hologram, impact)
+ else
+ weapon_choice.special_effects_fire(user, src, hologram, target_hologram)
+ if(!isnull(weapon_choice.maximum_ammo))
+ if(weapon_choice.maximum_ammo <= 0)
+ weaponry -= weapon_choice
+ report_no_ammo(hologram, weapon_choice)
+ qdel(weapon_choice)
+
+/// Speaks a voiceline about a weapon being out of ammo
+/datum/wargame_unit_stats/proc/report_no_ammo(obj/structure/wargame_hologram/hologram, datum/wargame_weapon/weapon)
+ var/list/voicelines = list(
+ "[weapon.weapon_name], ammo depleted.",
+ "We're out of [weapon.weapon_name] [commander]!",
+ "[capitalize(commander)]? [weapon.weapon_name], reporting ammunition zero.",
+ "Bingo ammo on [weapon.weapon_name], [commander].",
+ "That was the last of [weapon.weapon_name] we had!",
+ )
+ hologram.say(pick(voicelines))
+ playsound(hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+
+/// Calculates getting attacked, returns TRUE if the weapon actually made impact with the enemy, even if it did nothing
+/datum/wargame_unit_stats/proc/get_attacked(mob/living/user, obj/structure/wargame_hologram/attacking_hologram, obj/structure/wargame_hologram/hologram, datum/wargame_weapon/weapon_used)
+ if(isnull(weapon_used))
+ return FALSE
+ var/incoming_attack_roll = roll(weapon_used.attack_roll)
+ if(is_small_vessel && weapon_used.small_ship_disadvantage)
+ incoming_attack_roll = min(incoming_attack_roll, roll(weapon_used.attack_roll))
+ var/total_armor_class = weapon_used.evadable ? (calculate_armor_class(hologram) + calculate_evasion_mod(hologram)) : calculate_armor_class(hologram)
+ if(incoming_attack_roll <= total_armor_class)
+ if(attacking_hologram.unit_stats.talkative)
+ attacking_hologram.say(missed_voiceline(attacking_hologram.unit_stats))
+ playsound(attacking_hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+ hologram.visible_message(span_warning("Attacker's roll, [weapon_used.attack_roll], resulted in [incoming_attack_roll], which was less than or equal to the target's effective armor class, [total_armor_class]."), \
+ blind_message = span_warning("Attacker's roll, [weapon_used.attack_roll], resulted in [incoming_attack_roll], which was less than or equal to the target's effective armor class, [total_armor_class]."))
+ return FALSE
+ if((incoming_attack_roll + weapon_used.damage_roll_bonus) <= armor_class)
+ if(attacking_hologram.unit_stats.talkative)
+ attacking_hologram.say(nonpen_voiceline(attacking_hologram.unit_stats))
+ playsound(attacking_hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+ hologram.visible_message(span_warning("Attacker's damage roll, [incoming_attack_roll + weapon_used.damage_roll_bonus], was less than or equal to the target's armor class, [armor_class]."), \
+ blind_message = span_warning("Attacker's damage roll, [incoming_attack_roll + weapon_used.damage_roll_bonus], was less than or equal to the target's armor class, [armor_class]."))
+ playsound(hologram, 'modular_doppler/wargaming/sound/ship_hit.ogg', 50, TRUE)
+ return TRUE // If our weapon is weak (PDC) then it makes our attack roll less than armor class
+ var/datum/wargame_condition/new_condition = pick(possible_conditions)
+ new_condition = new new_condition()
+ new_condition.applied_to_unit(src, hologram)
+ current_conditions += new_condition
+ if(attacking_hologram.unit_stats.talkative)
+ attacking_hologram.say(good_hit_voiceline(attacking_hologram.unit_stats))
+ playsound(attacking_hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+ hologram.visible_message(span_warning("Attacker's roll, [weapon_used.attack_roll], resulted in [incoming_attack_roll], which was higher than the target's effective armor class, [total_armor_class]."), \
+ blind_message = span_warning("Attacker's roll, [weapon_used.attack_roll], resulted in [incoming_attack_roll], which was higher than the target's effective armor class, [total_armor_class]."))
+ hologram.Shake(2, 0, 2 SECONDS)
+ playsound(hologram, 'modular_doppler/wargaming/sound/ship_hit.ogg', 50, TRUE)
+ return TRUE
+
+/// Returns a voiceline for missing the target
+/datum/wargame_unit_stats/proc/missed_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Bad target track, we missed the target [stats.commander]!",
+ "[capitalize(stats.commander)], we have missed the target.",
+ "Looks like we missed!",
+ )
+ return pick(lines)
+
+/// Returns a voiceline for hitting the target and doing nothing
+/datum/wargame_unit_stats/proc/nonpen_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Hit- We didn't even scratch them!",
+ "No effect on the target, bad hit [stats.commander]!",
+ "All we did was scratch the paint!",
+ )
+ return pick(lines)
+
+/// Returns a voiceline for hitting the target and damaging it
+/datum/wargame_unit_stats/proc/good_hit_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Confirming good hits [stats.commander].",
+ "[capitalize(stats.commander)]! We hit them! Good effect on target!",
+ "Yes, a hit!",
+ )
+ return pick(lines)
+
+/// Calculates the ship's armor class based on cover on the same tile
+/datum/wargame_unit_stats/proc/calculate_armor_class(obj/hologram)
+ var/cover_modifier = 0
+ var/turf/cover_turf = get_turf(hologram)
+ for(var/obj/structure/wargame_hologram/cover_hologram in cover_turf.contents)
+ if(cover_hologram == hologram)
+ continue
+ if(cover_hologram.unit_stats.unit_size > unit_size)
+ cover_modifier++
+ return armor_class + min(cover_modifier, WARGAME_MAX_COVER_BONUS)
+
+/// Calculates the ship's evasion bonus based on evasion boosters in the tile
+/datum/wargame_unit_stats/proc/calculate_evasion_mod(obj/hologram)
+ var/evasion_modifier = 0
+ var/turf/evasion_turf = get_turf(hologram)
+ for(var/obj/structure/wargame_hologram/evasion_hologram in evasion_turf.contents)
+ if(evasion_hologram.unit_stats.unit_size == WARGAME_EVASION_BONUS)
+ evasion_modifier++
+ return evasion_modifier + min(evasion_modifier, WARGAME_MAX_EVASION_BONUS)
+
+/// Pulls up an 8 dir radial wheel for movement, also handles checking if we have the action points to move or not
+/datum/wargame_unit_stats/proc/unit_movement(mob/living/user, obj/hologram)
+ if(isnull(hologram))
+ return // ??
+ if(movement_cost > action_points)
+ hologram.balloon_alert(user, "not enough AP!")
+ return
+ var/picked_dir_string = show_radial_menu(user, hologram, GLOB.all_radial_directions)
+ if(isnull(picked_dir_string))
+ return
+ var/picked_dir = text2dir(picked_dir_string)
+ try_move_adjacent(hologram, picked_dir)
+ action_points -= movement_cost
+ post_move_effects(hologram)
+
+/// Any effects to do post movement, like voicelines or sound effects
+/datum/wargame_unit_stats/proc/post_move_effects(obj/hologram)
+ if(prob(50))
+ playsound(hologram, 'sound/items/radio/radio_talk.ogg', 50, TRUE)
+ return
+ var/list/lines = list(
+ "Moving to designated coordinates.",
+ "Moving to position [commander].",
+ "Displacing.",
+ "Getting there.",
+ "Starting burn.",
+ "Move order ackowledged.",
+ "Course set [commander].",
+ "Coordinates dialed in.",
+ )
+ if(talkative)
+ hologram.say("[pick(lines)]")
+ playsound(hologram, 'sound/items/radio/radio_receive.ogg', 50, TRUE)
+
+/datum/wargame_unit_stats/generic
+ unit_class = "Generic Thing"
+ generates_name = TRUE
+ unit_name = "Unidentified Thing"
+ conditions_limit = 2
+ possible_conditions = list(
+ /datum/wargame_condition/hull_damage,
+ )
+ armor_class = 5
+ evasion_modifier = 0
+ movement_cost = 1
+ maximum_action_points = 3
+ unit_size = WARGAME_SIZE_MEDIUM
+ weaponry = list(
+ /datum/wargame_weapon/medium_cannon,
+ /datum/wargame_weapon/pdc,
+ )
diff --git a/modular_doppler/wargaming/code/unit_stats/asteroid.dm b/modular_doppler/wargaming/code/unit_stats/asteroid.dm
new file mode 100644
index 00000000000000..4504935d7d694d
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/asteroid.dm
@@ -0,0 +1,41 @@
+/datum/wargame_unit_stats/terrain
+ abstract_type = /datum/wargame_unit_stats/terrain
+ conditions_limit = 4
+ possible_conditions = list(
+ /datum/wargame_condition/hull_damage/asteroid,
+ )
+ is_small_vessel = FALSE
+ unit_size = WARGAME_SIZE_LARGE
+ talkative = FALSE
+
+/datum/wargame_unit_stats/terrain/get_unit_basic_actions()
+ return
+
+/datum/wargame_unit_stats/terrain/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ hologram.visible_message(span_warning("[hologram] cracks into pieces before fading into a digital cloud!"), \
+ blind_message = span_warning("[hologram] cracks into pieces before fading into a digital cloud!"))
+ return ..()
+
+/datum/wargame_unit_stats/terrain/asteroid
+ unit_class = "asteroid"
+ unit_description = "A large asteroid that serves as good cover. Can be broken apart with sufficient damage."
+ generates_name = TRUE
+ armor_class = 13
+ evasion_modifier = 0
+ movement_cost = 100
+ maximum_action_points = 0
+ weaponry = list()
+
+/datum/wargame_unit_stats/terrain/asteroid/create_unit_name(datum/wargaming_team/team)
+ return "[pick_list_replacements("~doppler/wargame_identifiers.json", "name_word")] [rand(100, 999)]"
+
+/datum/wargame_unit_stats/terrain/dust_cloud
+ unit_class = "dust cloud"
+ unit_description = "A large cloud of space dust of some kind. While unserviceable as cover, gives a boost to evasion while inside of it."
+ armor_class = 200
+ evasion_modifier = 0
+ movement_cost = 100
+ maximum_action_points = 0
+ can_be_a_target = FALSE
+ unit_size = WARGAME_EVASION_BONUS
+ weaponry = list()
diff --git a/modular_doppler/wargaming/code/unit_stats/corvette.dm b/modular_doppler/wargaming/code/unit_stats/corvette.dm
new file mode 100644
index 00000000000000..86a017d7f8f4c7
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/corvette.dm
@@ -0,0 +1,29 @@
+/datum/wargame_unit_stats/corvette
+ abstract_type = /datum/wargame_unit_stats/corvette
+ generates_name = TRUE
+ conditions_limit = 2
+ possible_conditions = list(
+ /datum/wargame_condition/hull_damage,
+ /datum/wargame_condition/engine_damage,
+ /datum/wargame_condition/reactor_damage,
+ )
+ is_small_vessel = TRUE
+ unit_size = WARGAME_SIZE_SMALL
+
+/datum/wargame_unit_stats/corvette/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ hologram.visible_message(span_warning("[hologram] flashes and disappears as its reactor goes critical!"), \
+ blind_message = span_warning("[hologram] flashes and disappears as its reactor goes critical!"))
+ return ..()
+
+/datum/wargame_unit_stats/corvette/picket
+ unit_class = "picket corvette"
+ unit_description = "A corvette fitted with PDC for fleet defence."
+ armor_class = 8
+ evasion_modifier = 2
+ movement_cost = 1
+ maximum_action_points = 3
+ weaponry = list(
+ /datum/wargame_weapon/pdc,
+ /datum/wargame_weapon/autocannon,
+ /datum/wargame_weapon/missile/torpedo/small,
+ )
diff --git a/modular_doppler/wargaming/code/unit_stats/cruiser.dm b/modular_doppler/wargaming/code/unit_stats/cruiser.dm
new file mode 100644
index 00000000000000..2ada996e8a33aa
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/cruiser.dm
@@ -0,0 +1,60 @@
+/datum/wargame_unit_stats/cruiser
+ abstract_type = /datum/wargame_unit_stats/cruiser
+ generates_name = TRUE
+ conditions_limit = 4
+ possible_conditions = list(
+ /datum/wargame_condition/hull_damage,
+ /datum/wargame_condition/engine_damage,
+ /datum/wargame_condition/reactor_damage,
+ )
+ is_small_vessel = FALSE
+ unit_size = WARGAME_SIZE_LARGE
+
+/datum/wargame_unit_stats/cruiser/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ hologram.visible_message(span_warning("[hologram] starts to drift off course before shattering into a large debris field!"), \
+ blind_message = span_warning("[hologram] starts to drift off course before shattering into a large debris field!"))
+ return ..()
+
+/datum/wargame_unit_stats/cruiser/mass_driver
+ unit_class = "mass driver cruiser"
+ unit_description = "A cruiser equipped with a heavy mass driver."
+ armor_class = 13
+ evasion_modifier = 0
+ movement_cost = 1
+ maximum_action_points = 4
+ weaponry = list(
+ /datum/wargame_weapon/pd_beam,
+ /datum/wargame_weapon/missile/swarm,
+ /datum/wargame_weapon/mass_driver,
+ /datum/wargame_weapon/autocannon,
+ )
+
+/datum/wargame_unit_stats/cruiser/artillery
+ unit_class = "artillery cruiser"
+ unit_description = "A cruiser equipped with heavy long-range artillery."
+ armor_class = 13
+ evasion_modifier = 0
+ movement_cost = 1
+ maximum_action_points = 4
+ weaponry = list(
+ /datum/wargame_weapon/pd_beam,
+ /datum/wargame_weapon/missile/swarm,
+ /datum/wargame_weapon/large_cannon,
+ /datum/wargame_weapon/medium_cannon,
+ )
+
+/datum/wargame_unit_stats/cruiser/linebreaker
+ unit_class = "battlecruiser"
+ unit_description = "A heavily armed and armored cruiser for breaking through fortified enemy lines."
+ armor_class = 14
+ evasion_modifier = 0
+ movement_cost = 2
+ maximum_action_points = 4
+ weaponry = list(
+ /datum/wargame_weapon/missile/swarm,
+ /datum/wargame_weapon/missile/torpedo,
+ /datum/wargame_weapon/medium_cannon,
+ /datum/wargame_weapon/pdc,
+ /datum/wargame_weapon/autocannon,
+ /datum/wargame_weapon/anti_ship_beam,
+ )
diff --git a/modular_doppler/wargaming/code/unit_stats/destroyer.dm b/modular_doppler/wargaming/code/unit_stats/destroyer.dm
new file mode 100644
index 00000000000000..208a2bd3923494
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/destroyer.dm
@@ -0,0 +1,56 @@
+/datum/wargame_unit_stats/destroyer
+ abstract_type = /datum/wargame_unit_stats/destroyer
+ generates_name = TRUE
+ conditions_limit = 4
+ possible_conditions = list(
+ /datum/wargame_condition/hull_damage,
+ /datum/wargame_condition/engine_damage,
+ /datum/wargame_condition/reactor_damage,
+ )
+ is_small_vessel = FALSE
+ unit_size = WARGAME_SIZE_MEDIUM
+
+/datum/wargame_unit_stats/destroyer/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ hologram.visible_message(span_warning("[hologram] starts to drift off course before shattering into a large debris field!"), \
+ blind_message = span_warning("[hologram] starts to drift off course before shattering into a large debris field!"))
+ return ..()
+
+/datum/wargame_unit_stats/destroyer/brawler
+ unit_class = "destroyer"
+ unit_description = "A heavily armored destroyer for close-in brawling engagements."
+ armor_class = 12
+ evasion_modifier = 0
+ movement_cost = 1
+ maximum_action_points = 4
+ weaponry = list(
+ /datum/wargame_weapon/autocannon,
+ /datum/wargame_weapon/missile/swarm,
+ /datum/wargame_weapon/medium_cannon,
+ /datum/wargame_weapon/missile/torpedo/small,
+ )
+
+/datum/wargame_unit_stats/destroyer/beam
+ unit_class = "beam frigate"
+ unit_description = "A frigate equipped with special anti-ship beam weaponry."
+ armor_class = 10
+ evasion_modifier = 0
+ movement_cost = 1
+ maximum_action_points = 4
+ weaponry = list(
+ /datum/wargame_weapon/missile/swarm,
+ /datum/wargame_weapon/anti_ship_beam,
+ /datum/wargame_weapon/pd_beam,
+ )
+
+/datum/wargame_unit_stats/destroyer/missile
+ unit_class = "missile frigate"
+ unit_description = "A frigate loaded with lines and lines of launch tubes for various missiles."
+ armor_class = 10
+ evasion_modifier = 0
+ movement_cost = 1
+ maximum_action_points = 4
+ weaponry = list(
+ /datum/wargame_weapon/pdc,
+ /datum/wargame_weapon/missile/cruise,
+ /datum/wargame_weapon/missile/swarm,
+ )
diff --git a/modular_doppler/wargaming/code/unit_stats/missiles.dm b/modular_doppler/wargaming/code/unit_stats/missiles.dm
new file mode 100644
index 00000000000000..6c734f3ffb2681
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/missiles.dm
@@ -0,0 +1,63 @@
+/datum/wargame_unit_stats/missile
+ abstract_type = /datum/wargame_unit_stats/missile
+ talkative = FALSE
+ generates_name = TRUE
+
+/datum/wargame_unit_stats/missile/create_unit_name(datum/wargaming_team/team)
+ return "Track [pick_list_replacements("~doppler/wargame_identifiers.json", "name_word")] [rand(1,99)]"
+
+/datum/wargame_unit_stats/missile/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ hologram.visible_message(span_warning("[hologram] is reduced to data with a bright spark!"), \
+ blind_message = span_warning("[hologram] is reduced to data with a bright spark!"))
+ return ..()
+
+/datum/wargame_unit_stats/missile/cruise
+ unit_class = "cruise missile"
+ unit_description = "A long range cruise missile."
+ conditions_limit = 0 // Anything will instantly destroy this
+ possible_conditions = list(
+ /datum/wargame_condition/missile_failure,
+ )
+ armor_class = 4
+ evasion_modifier = 1
+ movement_cost = 1
+ maximum_action_points = 3
+ is_small_vessel = TRUE
+ unit_size = WARGAME_SIZE_SMALL
+ weaponry = list(
+ /datum/wargame_weapon/ramming/cruise,
+ )
+
+/datum/wargame_unit_stats/missile/swarm
+ unit_class = "swarm missiles"
+ unit_description = "A swarm of small interceptor missiles."
+ conditions_limit = 0 // Anything will instantly destroy this
+ possible_conditions = list(
+ /datum/wargame_condition/missile_failure,
+ )
+ armor_class = 2
+ evasion_modifier = 2
+ movement_cost = 1
+ maximum_action_points = 2
+ is_small_vessel = TRUE
+ unit_size = WARGAME_SIZE_SMALL
+ weaponry = list(
+ /datum/wargame_weapon/ramming/swarm,
+ )
+
+/datum/wargame_unit_stats/missile/torpedo
+ unit_class = "torpedo"
+ unit_description = "A large anti-capital ship torpedo."
+ conditions_limit = 0 // Anything will instantly destroy this
+ possible_conditions = list(
+ /datum/wargame_condition/missile_failure,
+ )
+ armor_class = 5
+ evasion_modifier = 0
+ movement_cost = 1
+ maximum_action_points = 2
+ is_small_vessel = TRUE
+ unit_size = WARGAME_SIZE_SMALL
+ weaponry = list(
+ /datum/wargame_weapon/ramming/torpedo,
+ )
diff --git a/modular_doppler/wargaming/code/unit_stats/static.dm b/modular_doppler/wargaming/code/unit_stats/static.dm
new file mode 100644
index 00000000000000..4623bcde04c5dc
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/static.dm
@@ -0,0 +1,61 @@
+/datum/wargame_unit_stats/platform
+ abstract_type = /datum/wargame_unit_stats/platform
+ generates_name = FALSE
+ conditions_limit = 4
+ possible_conditions = list(
+ /datum/wargame_condition/hull_damage,
+ /datum/wargame_condition/reactor_damage,
+ )
+ is_small_vessel = FALSE
+ unit_size = WARGAME_SIZE_MEDIUM
+
+/datum/wargame_unit_stats/platform/get_unit_basic_actions()
+ var/static/our_actions = list(
+ WARGAME_UNIT_ATTACK = image(icon = WARGAME_ACTIONS_FILE, icon_state = "unit_attack"),
+ )
+ return our_actions
+
+/datum/wargame_unit_stats/platform/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ hologram.visible_message(span_warning("[hologram] briefly becomes a new star in the night sky, before fading into a large debris field!"), \
+ blind_message = span_warning("[hologram] briefly becomes a new star in the night sky, before fading into a large debris field!"))
+ return ..()
+
+/datum/wargame_unit_stats/platform/outpost
+ unit_class = "outpost platform"
+ unit_description = "A stationary defence platform that trades movement for armor and armament."
+ armor_class = 11
+ evasion_modifier = 0
+ movement_cost = 1000
+ maximum_action_points = 2
+ weaponry = list(
+ /datum/wargame_weapon/autocannon,
+ /datum/wargame_weapon/rockets,
+ /datum/wargame_weapon/pdc,
+ )
+
+/datum/wargame_unit_stats/platform/civilian
+ unit_class = "civilian station"
+ unit_description = "A civilian station, only lightly armed to deter asteroids and brigands."
+ armor_class = 7
+ evasion_modifier = 0
+ movement_cost = 1000
+ maximum_action_points = 2
+ weaponry = list(
+ /datum/wargame_weapon/pdc,
+ /datum/wargame_weapon/pd_beam,
+ )
+
+/datum/wargame_unit_stats/platform/citadel
+ unit_class = "military station"
+ unit_description = "An armed military station, highly dangerous even without outside support."
+ armor_class = 15
+ evasion_modifier = 0
+ movement_cost = 1000
+ maximum_action_points = 4
+ weaponry = list(
+ /datum/wargame_weapon/mass_driver,
+ /datum/wargame_weapon/medium_cannon,
+ /datum/wargame_weapon/pd_beam,
+ /datum/wargame_weapon/missile/cruise,
+ /datum/wargame_weapon/missile/swarm,
+ )
diff --git a/modular_doppler/wargaming/code/unit_stats/strike.dm b/modular_doppler/wargaming/code/unit_stats/strike.dm
new file mode 100644
index 00000000000000..6844e0eaa04e96
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_stats/strike.dm
@@ -0,0 +1,47 @@
+/datum/wargame_unit_stats/strike
+ abstract_type = /datum/wargame_unit_stats/strike
+ generates_name = TRUE
+ conditions_limit = 2
+ possible_conditions = list(
+ /datum/wargame_condition/hull_damage,
+ /datum/wargame_condition/engine_damage,
+ /datum/wargame_condition/reactor_damage,
+ )
+ is_small_vessel = TRUE
+ unit_size = WARGAME_SIZE_SMALL
+
+/datum/wargame_unit_stats/strike/im_boutta_blow(obj/structure/wargame_hologram/hologram)
+ hologram.visible_message(span_warning("[hologram] starts to careen off course before crackling and exploding!"), \
+ blind_message = span_warning("[hologram] starts to careen off course before crackling and exploding!"))
+ return ..()
+
+/datum/wargame_unit_stats/strike/create_unit_name(datum/wargaming_team/team)
+ return "Squadron [pick_list_replacements("~doppler/wargame_identifiers.json", "name_word")] [rand(1, 9)]"
+
+/datum/wargame_unit_stats/strike/wing
+ unit_class = "strike wing"
+ unit_description = "A strike wing of bombers and its fighter escorts."
+ armor_class = 7
+ evasion_modifier = 4
+ movement_cost = 1
+ maximum_action_points = 3
+ weaponry = list(
+ /datum/wargame_weapon/pdc,
+ /datum/wargame_weapon/rockets,
+ /datum/wargame_weapon/bombs,
+ /datum/wargame_weapon/ramming/strike,
+ )
+
+/datum/wargame_unit_stats/strike/interceptor
+ unit_class = "interceptor wing"
+ unit_description = "An interceptor wing for interdicting other strike wings or incoming missiles."
+ armor_class = 6
+ evasion_modifier = 5
+ movement_cost = 1
+ maximum_action_points = 3
+ weaponry = list(
+ /datum/wargame_weapon/pdc,
+ /datum/wargame_weapon/autocannon,
+ /datum/wargame_weapon/missile/swarm/strike,
+ /datum/wargame_weapon/ramming/strike,
+ )
diff --git a/modular_doppler/wargaming/code/unit_weapons/_unit_weapons.dm b/modular_doppler/wargaming/code/unit_weapons/_unit_weapons.dm
new file mode 100644
index 00000000000000..17f35ea3c02d97
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_weapons/_unit_weapons.dm
@@ -0,0 +1,135 @@
+/datum/wargame_weapon
+ abstract_type = /datum/wargame_weapon
+ /// The name of the weapon
+ var/weapon_name
+ /// The attack roll of the weapon if within range
+ var/attack_roll
+ /// A negative number to make weapons that are weak to armor less effective against high armor class
+ var/damage_roll_bonus
+ /// The weapon range in tiles, 0 is the same tile as the ship firing it
+ var/attack_range
+ /// If this attack can be evaded, giving ships with an evasion bonus extra armor class
+ var/evadable
+ /// If this weapon has roll disadvantage against small ships
+ var/small_ship_disadvantage = FALSE
+ /// The icon_state to use for this weapon's radial menu option
+ var/radial_icon_state
+ /// If this weapon has a limited number of uses before needing to be re-armed
+ var/maximum_ammo
+ /// How many action points this weapon costs to fire
+ var/action_point_cost
+ /// If this weapon's attack is handled entirely by special_effects_fire()
+ var/all_special_effects = FALSE
+
+/// Returns a string to describe the weapon
+/datum/wargame_weapon/proc/weapon_description()
+ return
+
+/// Returns a string for a ship to say when firing the weapon
+/datum/wargame_weapon/proc/firing_voiceline(datum/wargame_unit_stats/stats)
+ return
+
+/// Plays a firing sound for the weapon
+/datum/wargame_weapon/proc/weapon_firing_sound(obj/firer)
+ return
+
+/// Plays a firing sound for the weapon
+/datum/wargame_weapon/proc/weapon_firing_message(obj/firer, obj/target)
+ return
+
+/// By default, checks in a circle range around the ship for another hologram to target
+/datum/wargame_weapon/proc/pick_target(mob/living/user, obj/structure/wargame_hologram/hologram)
+ var/datum/wargaming_team/hologram_team = hologram.team_reference?.resolve()
+ var/list/potential_targets = list()
+ var/list/target_ships = list()
+ var/list/target_missiles = list()
+ var/list/every_other_target = list()
+ for(var/obj/structure/wargame_hologram/other_hologram in range(attack_range, hologram))
+ if(!other_hologram.unit_stats?.can_be_a_target)
+ continue // Anything we target at all should have unit stats but you never know
+ if(isnull(hologram_team))
+ if(other_hologram.controllable && !istype(other_hologram.unit_stats, /datum/wargame_unit_stats/missile))
+ target_ships += other_hologram
+ else if(istype(other_hologram.unit_stats, /datum/wargame_unit_stats/missile))
+ target_missiles += other_hologram
+ else
+ every_other_target += other_hologram
+ continue // If we have no team then free for all everything's a target
+ var/datum/wargaming_team/other_hologram_team = other_hologram.team_reference?.resolve()
+ if(isnull(other_hologram_team))
+ if(other_hologram.controllable && !istype(other_hologram.unit_stats, /datum/wargame_unit_stats/missile))
+ target_ships += other_hologram
+ else if(istype(other_hologram.unit_stats, /datum/wargame_unit_stats/missile))
+ target_missiles += other_hologram
+ else
+ every_other_target += other_hologram
+ if(other_hologram_team != hologram_team)
+ if(other_hologram.controllable && !istype(other_hologram.unit_stats, /datum/wargame_unit_stats/missile))
+ target_ships += other_hologram
+ else if(istype(other_hologram.unit_stats, /datum/wargame_unit_stats/missile))
+ target_missiles += other_hologram
+ else
+ every_other_target += other_hologram
+ // Sorts targets so ships and missiles are at the top
+ potential_targets = target_ships + target_missiles + every_other_target
+ if(!length(potential_targets))
+ hologram.balloon_alert(user, "no targets!")
+ return
+ var/picked_target = tgui_input_list(user, "Pick a target within range.", "Target Picker", potential_targets)
+ if(isnull(picked_target))
+ hologram.balloon_alert(user, "no choice!")
+ return
+ return picked_target
+
+/// If this weapon has any special pre-fire checks
+/datum/wargame_weapon/proc/prefire_checks(mob/living/user, datum/wargame_unit_stats/stats, obj/structure/wargame_hologram/hologram, obj/structure/wargame_hologram/target_hologram)
+ if(stats.action_points < action_point_cost)
+ hologram.balloon_alert(user, "not enough AP!")
+ return FALSE
+ if(get_dist(hologram, target_hologram) > attack_range)
+ hologram.balloon_alert(user, "out of range!")
+ if(isnull(maximum_ammo))
+ return TRUE
+ if(maximum_ammo <= 0)
+ hologram.balloon_alert(user, "no ammo!")
+ return FALSE
+ return TRUE
+
+/// If this weapon does anything special when initially firing
+/datum/wargame_weapon/proc/special_effects_fire(mob/living/user, datum/wargame_unit_stats/stats, obj/structure/wargame_hologram/hologram, obj/structure/wargame_hologram/target_hologram, impact)
+ stats.action_points -= action_point_cost
+ if(isnull(maximum_ammo))
+ return
+ maximum_ammo--
+
+// Generic ramming weapon for missiles
+/datum/wargame_weapon/ramming
+ abstract_type = /datum/wargame_weapon/ramming
+ weapon_name = "Ramming Speed"
+ attack_range = 1
+ radial_icon_state = "weapon_ram"
+
+/datum/wargame_weapon/ramming/special_effects_fire(mob/living/user, datum/wargame_unit_stats/stats, obj/structure/wargame_hologram/hologram, obj/structure/wargame_hologram/target_hologram, impact)
+ stats.im_boutta_blow(hologram)
+
+// Generic missile launch weapon
+/datum/wargame_weapon/missile
+ abstract_type = /datum/wargame_weapon/missile
+ attack_range = 7
+ all_special_effects = TRUE
+ /// The type of missile hologram this should spawn when running an attack outside of short range
+ var/obj/structure/wargame_hologram/missile_type
+
+/datum/wargame_weapon/missile/special_effects_fire(mob/living/user, datum/wargame_unit_stats/stats, obj/structure/wargame_hologram/hologram, obj/structure/wargame_hologram/target_hologram, impact)
+ if(!target_hologram)
+ return // ??
+ if(get_dist(hologram, target_hologram) <= 1)
+ target_hologram.unit_stats.get_attacked(user, hologram, target_hologram, src)
+ return ..()
+ var/dir_to_target = get_dir(hologram, target_hologram)
+ var/obj/structure/wargame_hologram/new_missile = new missile_type(get_turf(hologram), hologram.projector)
+ new_missile.team_reference = hologram.team_reference
+ new_missile.color = hologram.color
+ new_missile.update_appearance()
+ try_move_adjacent(new_missile, dir_to_target)
+ return ..()
diff --git a/modular_doppler/wargaming/code/unit_weapons/beams.dm b/modular_doppler/wargaming/code/unit_weapons/beams.dm
new file mode 100644
index 00000000000000..f4f9b4c169bee1
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_weapons/beams.dm
@@ -0,0 +1,54 @@
+/datum/wargame_weapon/anti_ship_beam
+ weapon_name = "Anti-Ship Beam"
+ attack_roll = "2d12+7"
+ damage_roll_bonus = 0
+ attack_range = 1
+ evadable = FALSE
+ small_ship_disadvantage = TRUE
+ radial_icon_state = "weapon_beam"
+ action_point_cost = 1
+
+/datum/wargame_weapon/anti_ship_beam/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/beam.ogg', 40, TRUE)
+
+/datum/wargame_weapon/anti_ship_beam/weapon_description()
+ return "A high-energy beam for coring large ships with. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/anti_ship_beam/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer]'s spinal beam lens glows before piercing the sky towards [target] with a blinding flash of light!"), \
+ blind_message = span_warning("[firer]'s spinal beam lens glows before piercing the sky towards [target] with a blinding flash of light!"))
+
+/datum/wargame_weapon/anti_ship_beam/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Try not to stare directly at the beam [stats.commander].",
+ "Hope you brought your sunglasses today [stats.commander].",
+ "Plasma cutters got nothing on me, watch this.",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/pd_beam
+ weapon_name = "PD Beam"
+ attack_roll = "1d20+6"
+ damage_roll_bonus = -5
+ attack_range = 2
+ evadable = FALSE
+ radial_icon_state = "weapon_pd_beam"
+ action_point_cost = 1
+
+/datum/wargame_weapon/pd_beam/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/beam.ogg', 40, TRUE)
+
+/datum/wargame_weapon/pd_beam/weapon_description()
+ return "Highly focused beams with exceptional hit chance but low damage. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/pd_beam/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("Gimbaled lenses all along [firer] turn towards [target] and light up the night with a dazzling laser light show!"), \
+ blind_message = span_warning("Gimbaled lenses all along [firer] turn towards [target] and light up the night with a dazzling laser light show!"))
+
+/datum/wargame_weapon/pd_beam/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Incoming track within range, blink it with the lasers.",
+ "Get the spotlights on that track, I want it off the board!",
+ "Laser defence responding to incoming track [stats.commander].",
+ )
+ return pick(lines)
diff --git a/modular_doppler/wargaming/code/unit_weapons/big_guns.dm b/modular_doppler/wargaming/code/unit_weapons/big_guns.dm
new file mode 100644
index 00000000000000..c51004b0afc27f
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_weapons/big_guns.dm
@@ -0,0 +1,85 @@
+/datum/wargame_weapon/mass_driver
+ weapon_name = "Mass Driver"
+ attack_roll = "2d12+2"
+ damage_roll_bonus = 0
+ attack_range = 3
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ radial_icon_state = "weapon_guns_big"
+ action_point_cost = 2
+
+/datum/wargame_weapon/mass_driver/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/cannon.ogg', 40, TRUE)
+
+/datum/wargame_weapon/mass_driver/weapon_description()
+ return "A large mass driver for launching hardened cores at large targets. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/mass_driver/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer]'s mass driver ripples the space around it as it charges, piercing a streak across the sky as it fires at [target]!"), \
+ blind_message = span_warning("[firer]'s mass driver ripples the space around it as it charges, piercing a streak across the sky as it fires at [target]!"))
+
+/datum/wargame_weapon/mass_driver/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "We shot this in atmosphere once, you should've seen it [stats.commander]!",
+ "One-second delivery, or your tungsten rod is free!",
+ "Let's see how that ship looks with a brand new hole down the center of it.",
+ "Tungsten cube, hot and ready for them [stats.commander]!",
+ "[capitalize(stats.commander)], a few more shots like this, and we'll have to change the rails out.",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/large_cannon
+ weapon_name = "7\" Cannon"
+ attack_roll = "2d16+4"
+ damage_roll_bonus = 0
+ attack_range = 3
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ radial_icon_state = "weapon_guns_big"
+ action_point_cost = 2
+
+/datum/wargame_weapon/large_cannon/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/cannon.ogg', 40, TRUE)
+
+/datum/wargame_weapon/large_cannon/weapon_description()
+ return "A large calibre 7 in. cannon for launching shells at large targets. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/large_cannon/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer]'s 7\" guns fire at [target], sending a jet of burning plasma out the back of each cannon!"), \
+ blind_message = span_warning("[firer]'s 7\" guns fire at [target], sending a jet of burning plasma out the back of each cannon!"))
+
+/datum/wargame_weapon/large_cannon/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Target lead and trajectory calculations green, fire at will.",
+ "Artillery adds dignity to what would otherwise be a vulgar brawl",
+ "Shell away. Have we ceased life, [stats.commander]?",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/medium_cannon
+ weapon_name = "2.5\" Cannon"
+ attack_roll = "4d5+4"
+ damage_roll_bonus = 0
+ attack_range = 2
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ radial_icon_state = "weapon_guns_big"
+ action_point_cost = 1
+
+/datum/wargame_weapon/medium_cannon/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/cannon.ogg', 40, TRUE)
+
+/datum/wargame_weapon/medium_cannon/weapon_description()
+ return "A medium calibre 2.5 in. cannon fore launching shells at large targets. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/medium_cannon/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer]'s 2.5\" guns turn to bear on [target], rattling the vessel as they unleash a volley of shells!"), \
+ blind_message = span_warning("[firer]'s 2.5\" guns turn to bear on [target], rattling the vessel as they unleash a volley of shells!"))
+
+/datum/wargame_weapon/medium_cannon/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "All mounts local control, watch fire for friendlies!",
+ "[capitalize(stats.commander)] wants all guns on that track, open fire!",
+ "Target in range, all guns to local fire director!",
+ )
+ return pick(lines)
diff --git a/modular_doppler/wargaming/code/unit_weapons/bombs.dm b/modular_doppler/wargaming/code/unit_weapons/bombs.dm
new file mode 100644
index 00000000000000..94fb5ef4f0f039
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_weapons/bombs.dm
@@ -0,0 +1,58 @@
+/datum/wargame_weapon/rockets
+ weapon_name = "Rockets"
+ attack_roll = "6d4"
+ damage_roll_bonus = 0
+ attack_range = 2
+ evadable = TRUE
+ radial_icon_state = "weapon_rocket"
+ action_point_cost = 1
+
+/datum/wargame_weapon/rockets/strike
+ maximum_ammo = 4
+
+/datum/wargame_weapon/rockets/weapon_description()
+ return "A barrage of unguided rockets from a fixed rack or large launcher. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/rockets/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/swarm_launch.ogg', 40, TRUE)
+
+/datum/wargame_weapon/rockets/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] lets loose a volley of rockets at [target]!"), \
+ blind_message = span_warning("[firer] lets loose a volley of rockets at [target]!"))
+
+/datum/wargame_weapon/rockets/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/static/list/lines = list(
+ "Go for rocket salvo, let's see how they like some of these!",
+ "Rockets! Rockets! You're in my party now!",
+ "Make it rain on them!",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/bombs
+ weapon_name = "Bombs"
+ attack_roll = "1d20"
+ damage_roll_bonus = 0
+ attack_range = 1
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ radial_icon_state = "weapon_bomb"
+ maximum_ammo = 2
+ action_point_cost = 1
+
+/datum/wargame_weapon/bombs/weapon_description()
+ return "A group of unpowered explosive bombs to crack large targets. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/bombs/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/cannon.ogg', 40, TRUE)
+
+/datum/wargame_weapon/bombs/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] makes a run at [target], releasing a large bomb that drifts toward them!"), \
+ blind_message = span_warning("[firer] makes a run at [target], releasing a large bomb that drifts toward them!"))
+
+/datum/wargame_weapon/bombs/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/static/list/lines = list(
+ "I got a present for ya!",
+ "Kill him! Kill him! Point the nose at him and drop the bomb at him!",
+ "Bombs away- Hard burn we're in the blast zone!",
+ )
+ return pick(lines)
diff --git a/modular_doppler/wargaming/code/unit_weapons/missiles.dm b/modular_doppler/wargaming/code/unit_weapons/missiles.dm
new file mode 100644
index 00000000000000..cdf3b071b7c0e3
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_weapons/missiles.dm
@@ -0,0 +1,141 @@
+/datum/wargame_weapon/missile/cruise
+ weapon_name = "Cruise Missile"
+ attack_roll = "1d20+4"
+ damage_roll_bonus = 0
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ radial_icon_state = "weapon_missile"
+ action_point_cost = 1
+ maximum_ammo = 6
+ missile_type = /obj/structure/wargame_hologram/controllable/cruise_missile
+
+/datum/wargame_weapon/missile/cruise/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/missile_launch.ogg', 30, TRUE)
+
+/datum/wargame_weapon/missile/cruise/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer]'s missile tubes pop open and dispense a large missile that quickly turns and burns towards [target]!"), \
+ blind_message = span_warning("[firer]'s missile tubes pop open and dispense a large missile that quickly turns and burns towards [target]!"))
+
+/datum/wargame_weapon/missile/cruise/weapon_description()
+ return "A long range cruise missile for engaging large targets. Becomes a controllable ship if the target is beyond short range. \
+ Max target range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/missile/cruise/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Drive signature? We'll have a cruise missile right up for them [stats.commander].",
+ "Drive cone locked, cruise missile away [stats.commander].",
+ "Lock confirmed, launching cruise missile [stats.commander].",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/ramming/cruise
+ weapon_name = "Cruise Missile Impact"
+ attack_roll = "1d20+4"
+ damage_roll_bonus = 0
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ action_point_cost = 1
+
+/datum/wargame_weapon/ramming/cruise/weapon_description()
+ return "Guide the cruise missile into a target. This is a one way trip. \
+ Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/ramming/cruise/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] engages its terminal stage boosters as it dives toward [target] in a spiraling loop!"), \
+ blind_message = span_warning("[firer] engages its terminal stage boosters as it dives toward [target] in a spiraling loop!"))
+
+/datum/wargame_weapon/missile/swarm
+ weapon_name = "Swarm Missiles"
+ attack_roll = "5d4+2"
+ damage_roll_bonus = -2
+ evadable = FALSE
+ radial_icon_state = "weapon_missile_small"
+ action_point_cost = 1
+ maximum_ammo = 6
+ missile_type = /obj/structure/wargame_hologram/controllable/swarm_missile
+
+/datum/wargame_weapon/missile/swarm/strike
+ maximum_ammo = 2
+
+/datum/wargame_weapon/missile/swarm/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/swarm_launch.ogg', 40, TRUE)
+
+/datum/wargame_weapon/missile/swarm/weapon_description()
+ return "A swarm of small missiles for intercepting other missiles or strike craft. Becomes a controllable ship if the target is beyond short range. \
+ Max target range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/missile/swarm/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] ejects a bouquet of micro-missiles with a puff of gas, which break off into a swarm towards [target]!"), \
+ blind_message = span_warning("[firer] ejects a bouquet of micro-missiles with a puff of gas, which break off into a swarm towards [target]!"))
+
+/datum/wargame_weapon/missile/swarm/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Get some swarmers on that track!",
+ "Try dodging the third, fourth, and fifth ones this time!",
+ "Solid track, swarmers out [stats.commander].",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/ramming/swarm
+ weapon_name = "Swarm Missiles Impact"
+ attack_roll = "5d4+2"
+ damage_roll_bonus = -2
+ evadable = FALSE
+ action_point_cost = 1
+
+/datum/wargame_weapon/ramming/swarm/weapon_description()
+ return "Guide the swarm missiles into a target. This is a one way trip. \
+ Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/ramming/swarm/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] swarm towards [target], burning in high-gee turns to evade defense!"), \
+ blind_message = span_warning("[firer] swarm towards [target], burning in high-gee turns to evade defense!"))
+
+/datum/wargame_weapon/missile/torpedo
+ weapon_name = "Torpedo"
+ attack_roll = "2d20+4"
+ damage_roll_bonus = 0
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ radial_icon_state = "weapon_torpedo"
+ action_point_cost = 1
+ maximum_ammo = 4
+ missile_type = /obj/structure/wargame_hologram/controllable/torpedo
+
+/datum/wargame_weapon/missile/torpedo/small
+ maximum_ammo = 2
+
+/datum/wargame_weapon/missile/torpedo/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/missile_launch.ogg', 30, TRUE)
+
+/datum/wargame_weapon/missile/torpedo/weapon_description()
+ return "A beefy torpedo for cracking large ships in two. Becomes a controllable ship if the target is beyond short range. \
+ Max target range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/missile/torpedo/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] dispenses a large torpedo, which slowly spins up and gimbals toward [target]!"), \
+ blind_message = span_warning("[firer] dispenses a large torpedo, which slowly spins up and gimbals toward [target]!"))
+
+/datum/wargame_weapon/missile/torpedo/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "The guns ain't enough. I want that track gone.",
+ "Torpedo away, stand by for fireworks [stats.commander].",
+ "Good track confirmed, torpedo dispensed [stats.commander].",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/ramming/torpedo
+ weapon_name = "Torpedo Impact"
+ attack_roll = "2d20+4"
+ damage_roll_bonus = 0
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ action_point_cost = 1
+
+/datum/wargame_weapon/ramming/torpedo/weapon_description()
+ return "Guide the torpedo into a target. This is a one way trip. \
+ Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/ramming/torpedo/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] ejects its cruise stage and ignites an oversized terminal booster, speeding towards [target]!"), \
+ blind_message = span_warning("[firer] ejects its cruise stage and ignites an oversized terminal booster, speeding towards [target]!"))
diff --git a/modular_doppler/wargaming/code/unit_weapons/ramming.dm b/modular_doppler/wargaming/code/unit_weapons/ramming.dm
new file mode 100644
index 00000000000000..6d198f097e85b5
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_weapons/ramming.dm
@@ -0,0 +1,24 @@
+/datum/wargame_weapon/ramming/strike
+ attack_roll = "5d5+2"
+ damage_roll_bonus = 0
+ evadable = TRUE
+ small_ship_disadvantage = TRUE
+ action_point_cost = 1
+
+/datum/wargame_weapon/ramming/strike/weapon_description()
+ return "Order the wing to fly into the target in a suicide attack. This is a one-way trip if successful. \
+ Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+
+/datum/wargame_weapon/ramming/strike/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "One. Last. Plan!",
+ "It's been an honor [stats.commander].",
+ "Initiating burn drive, godspeed [stats.commander].",
+ "Ah.",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/ramming/strike/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer] locks into a collision trajectory with [target], engines burning to full!"), \
+ blind_message = span_warning("[firer] locks into a collision trajectory with [target], engines burning to full!"))
diff --git a/modular_doppler/wargaming/code/unit_weapons/small_guns.dm b/modular_doppler/wargaming/code/unit_weapons/small_guns.dm
new file mode 100644
index 00000000000000..4657d37ece339b
--- /dev/null
+++ b/modular_doppler/wargaming/code/unit_weapons/small_guns.dm
@@ -0,0 +1,53 @@
+/datum/wargame_weapon/autocannon
+ weapon_name = "45mm Autocannon"
+ attack_roll = "2d10+2"
+ damage_roll_bonus = 0
+ attack_range = 1
+ evadable = TRUE
+ radial_icon_state = "weapon_guns"
+ action_point_cost = 1
+
+/datum/wargame_weapon/autocannon/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/autocannonlong.ogg', 40, TRUE)
+
+/datum/wargame_weapon/autocannon/weapon_description()
+ return "Low calibre 45mm autocannons for fast moving targets. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/autocannon/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer]'s 45mm autocannon tracks toward [target], following the target for mere moments before sending a burst of shells at it!"), \
+ blind_message = span_warning("[firer]'s 45mm autocannon tracks toward [target], following the target for mere moments before sending a burst of shells at it!"))
+
+/datum/wargame_weapon/autocannon/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "Tracking looks good, let's let 'em have some.",
+ "Autocannon belts spooled, ready to fire [stats.commander].",
+ "Like to see them dodge this one, shells away [stats.commander].",
+ )
+ return pick(lines)
+
+/datum/wargame_weapon/pdc
+ weapon_name = "26mm PDC"
+ attack_roll = "1d12+6"
+ damage_roll_bonus = -3
+ attack_range = 1
+ evadable = FALSE
+ radial_icon_state = "weapon_guns_pdc"
+ action_point_cost = 1
+
+/datum/wargame_weapon/pdc/weapon_firing_sound(obj/firer)
+ playsound(firer, 'modular_doppler/wargaming/sound/autocannonlong.ogg', 40, TRUE)
+
+/datum/wargame_weapon/pdc/weapon_description()
+ return "Small calibre 26mm autocannons with exceptional hit chance but low damage. Max range of [attack_range] tiles. [action_point_cost] AP to fire."
+
+/datum/wargame_weapon/pdc/weapon_firing_message(obj/firer, obj/target)
+ firer.visible_message(span_warning("[firer]'s point defence grid quickly gimbals toward [target], filling the sky with a dense rain of tracers!"), \
+ blind_message = span_warning("[firer]'s point defence grid quickly gimbals toward [target], filling the sky with a dense rain of tracers!"))
+
+/datum/wargame_weapon/pdc/firing_voiceline(datum/wargame_unit_stats/stats)
+ var/list/lines = list(
+ "We need PDC on that target, now!",
+ "One wall of lead, coming right up [stats.commander].",
+ "Tracking multiple targets [stats.commander], open fire.",
+ )
+ return pick(lines)
diff --git a/modular_doppler/wargaming/icons/actions.dmi b/modular_doppler/wargaming/icons/actions.dmi
new file mode 100644
index 00000000000000..f2c3bfdc391750
Binary files /dev/null and b/modular_doppler/wargaming/icons/actions.dmi differ
diff --git a/modular_doppler/wargaming/icons/holograms.dmi b/modular_doppler/wargaming/icons/holograms.dmi
new file mode 100644
index 00000000000000..7679aadedf805f
Binary files /dev/null and b/modular_doppler/wargaming/icons/holograms.dmi differ
diff --git a/modular_doppler/wargaming/icons/items.dmi b/modular_doppler/wargaming/icons/items.dmi
new file mode 100644
index 00000000000000..5bcfe3fb11fb03
Binary files /dev/null and b/modular_doppler/wargaming/icons/items.dmi differ
diff --git a/modular_doppler/wargaming/icons/mob/lefthand.dmi b/modular_doppler/wargaming/icons/mob/lefthand.dmi
new file mode 100644
index 00000000000000..b927250496ef9e
Binary files /dev/null and b/modular_doppler/wargaming/icons/mob/lefthand.dmi differ
diff --git a/modular_doppler/wargaming/icons/mob/righthand.dmi b/modular_doppler/wargaming/icons/mob/righthand.dmi
new file mode 100644
index 00000000000000..71f000d300048d
Binary files /dev/null and b/modular_doppler/wargaming/icons/mob/righthand.dmi differ
diff --git a/modular_doppler/wargaming/icons/mob/worn.dmi b/modular_doppler/wargaming/icons/mob/worn.dmi
new file mode 100644
index 00000000000000..494aee0e50a926
Binary files /dev/null and b/modular_doppler/wargaming/icons/mob/worn.dmi differ
diff --git a/modular_doppler/wargaming/icons/projectors_and_holograms.dmi b/modular_doppler/wargaming/icons/projectors_and_holograms.dmi
deleted file mode 100644
index 569b6d7274c9b5..00000000000000
Binary files a/modular_doppler/wargaming/icons/projectors_and_holograms.dmi and /dev/null differ
diff --git a/modular_doppler/wargaming/icons/turf.dmi b/modular_doppler/wargaming/icons/turf.dmi
new file mode 100644
index 00000000000000..d1fb9d9a8bba32
Binary files /dev/null and b/modular_doppler/wargaming/icons/turf.dmi differ
diff --git a/modular_doppler/wargaming/sound/attributions.txt b/modular_doppler/wargaming/sound/attributions.txt
new file mode 100644
index 00000000000000..ac9e33fc7e438a
--- /dev/null
+++ b/modular_doppler/wargaming/sound/attributions.txt
@@ -0,0 +1,8 @@
+All of the sounds listed are genuinely just generated with tones, waves, or noise with various fun patterns on them.
+autocannon.ogg
+beam.ogg
+cannon.ogg
+missile_launch.ogg
+ship_explode.ogg
+ship_hit.ogg
+swarm_launch.ogg
diff --git a/modular_doppler/wargaming/sound/autocannon.ogg b/modular_doppler/wargaming/sound/autocannon.ogg
new file mode 100644
index 00000000000000..a1b62c3e956f57
Binary files /dev/null and b/modular_doppler/wargaming/sound/autocannon.ogg differ
diff --git a/modular_doppler/wargaming/sound/autocannonlong.ogg b/modular_doppler/wargaming/sound/autocannonlong.ogg
new file mode 100644
index 00000000000000..f57538a9a2eb1c
Binary files /dev/null and b/modular_doppler/wargaming/sound/autocannonlong.ogg differ
diff --git a/modular_doppler/wargaming/sound/beam.ogg b/modular_doppler/wargaming/sound/beam.ogg
new file mode 100644
index 00000000000000..def94ad0b28693
Binary files /dev/null and b/modular_doppler/wargaming/sound/beam.ogg differ
diff --git a/modular_doppler/wargaming/sound/cannon.ogg b/modular_doppler/wargaming/sound/cannon.ogg
new file mode 100644
index 00000000000000..b7860bae334ec1
Binary files /dev/null and b/modular_doppler/wargaming/sound/cannon.ogg differ
diff --git a/modular_doppler/wargaming/sound/missile_launch.ogg b/modular_doppler/wargaming/sound/missile_launch.ogg
new file mode 100644
index 00000000000000..e67fb556078fc1
Binary files /dev/null and b/modular_doppler/wargaming/sound/missile_launch.ogg differ
diff --git a/modular_doppler/wargaming/sound/ship_explode.ogg b/modular_doppler/wargaming/sound/ship_explode.ogg
new file mode 100644
index 00000000000000..9cf5d7df788708
Binary files /dev/null and b/modular_doppler/wargaming/sound/ship_explode.ogg differ
diff --git a/modular_doppler/wargaming/sound/ship_hit.ogg b/modular_doppler/wargaming/sound/ship_hit.ogg
new file mode 100644
index 00000000000000..a355a1e77894be
Binary files /dev/null and b/modular_doppler/wargaming/sound/ship_hit.ogg differ
diff --git a/modular_doppler/wargaming/sound/swarm_launch.ogg b/modular_doppler/wargaming/sound/swarm_launch.ogg
new file mode 100644
index 00000000000000..ebfb4e87169df1
Binary files /dev/null and b/modular_doppler/wargaming/sound/swarm_launch.ogg differ
diff --git a/strings/~doppler/salvage_shuttle.json b/strings/~doppler/salvage_shuttle.json
index 8a723c8b558116..3264ab3f83182f 100644
--- a/strings/~doppler/salvage_shuttle.json
+++ b/strings/~doppler/salvage_shuttle.json
@@ -7,6 +7,14 @@
"@pick(designation) @pick(name_word)-@pick(name_word) @pick(digit)"
],
+ "ship_name_no_designation": [
+ "@pick(name_word) @pick(digit)",
+ "@pick(name_word) @pick(name_word)",
+ "@pick(name_word) @pick(name_word) @pick(digit)",
+ "@pick(name_word)-@pick(name_word)",
+ "@pick(name_word)-@pick(name_word) @pick(digit)"
+ ],
+
"designation": [
"4CA",
"4CAV",
diff --git a/strings/~doppler/wargame_identifiers.json b/strings/~doppler/wargame_identifiers.json
new file mode 100644
index 00000000000000..314e43b280f35e
--- /dev/null
+++ b/strings/~doppler/wargame_identifiers.json
@@ -0,0 +1,58 @@
+{
+ "name_word": [
+ "Ceres",
+ "Vesta",
+ "Pallas",
+ "Hygiea",
+ "Europa",
+ "Davida",
+ "Sylvia",
+ "Eunomia",
+ "Juno",
+ "Psyche",
+ "Thisbe",
+ "Doris",
+ "Fortuna",
+ "Themis",
+ "Alfa",
+ "Bravo",
+ "Charlie",
+ "Delta",
+ "Echo",
+ "Foxtrot",
+ "Golf",
+ "Hotel",
+ "India",
+ "Juliett",
+ "Kilo",
+ "Lima",
+ "Mike",
+ "November",
+ "Oscar",
+ "Papa",
+ "Quebec",
+ "Romeo",
+ "Sierra",
+ "Tango",
+ "Uniform",
+ "Victor",
+ "Whiskey",
+ "X-Ray",
+ "Yankee",
+ "Zulu",
+ "Able",
+ "Baker",
+ "Easy",
+ "Fox",
+ "Jig",
+ "King",
+ "Love",
+ "Queen",
+ "Roger",
+ "Sail",
+ "Sugar",
+ "Uncle",
+ "Yoke",
+ "Zebra"
+ ]
+}
diff --git a/tgstation.dme b/tgstation.dme
index dd28a59c785235..2e614c4d43b20f 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -474,6 +474,7 @@
#include "code\__DEFINES\~doppler_defines\traits.dm"
#include "code\__DEFINES\~doppler_defines\vehicles.dm"
#include "code\__DEFINES\~doppler_defines\vv.dm"
+#include "code\__DEFINES\~doppler_defines\wargames.dm"
#include "code\__DEFINES\~doppler_defines\wounds.dm"
#include "code\__DEFINES\~doppler_defines\traits\declarations.dm"
#include "code\__HELPERS\_auxtools_api.dm"
@@ -7953,9 +7954,35 @@
#include "modular_doppler\verbs\code\preferences.dm"
#include "modular_doppler\verbs\code\say.dm"
#include "modular_doppler\verbs\code\subtle.dm"
+#include "modular_doppler\wargaming\code\base_station.dm"
#include "modular_doppler\wargaming\code\game_kit.dm"
-#include "modular_doppler\wargaming\code\holograms.dm"
+#include "modular_doppler\wargaming\code\holodeck.dm"
#include "modular_doppler\wargaming\code\projectors.dm"
+#include "modular_doppler\wargaming\code\team_datums.dm"
+#include "modular_doppler\wargaming\code\conditions\_unit_conditions.dm"
+#include "modular_doppler\wargaming\code\holograms\_holograms.dm"
+#include "modular_doppler\wargaming\code\holograms\asteroid.dm"
+#include "modular_doppler\wargaming\code\holograms\corvette.dm"
+#include "modular_doppler\wargaming\code\holograms\cruiser.dm"
+#include "modular_doppler\wargaming\code\holograms\destroyer.dm"
+#include "modular_doppler\wargaming\code\holograms\missile.dm"
+#include "modular_doppler\wargaming\code\holograms\static.dm"
+#include "modular_doppler\wargaming\code\holograms\strike.dm"
+#include "modular_doppler\wargaming\code\unit_stats\_unit_stats.dm"
+#include "modular_doppler\wargaming\code\unit_stats\asteroid.dm"
+#include "modular_doppler\wargaming\code\unit_stats\corvette.dm"
+#include "modular_doppler\wargaming\code\unit_stats\cruiser.dm"
+#include "modular_doppler\wargaming\code\unit_stats\destroyer.dm"
+#include "modular_doppler\wargaming\code\unit_stats\missiles.dm"
+#include "modular_doppler\wargaming\code\unit_stats\static.dm"
+#include "modular_doppler\wargaming\code\unit_stats\strike.dm"
+#include "modular_doppler\wargaming\code\unit_weapons\_unit_weapons.dm"
+#include "modular_doppler\wargaming\code\unit_weapons\beams.dm"
+#include "modular_doppler\wargaming\code\unit_weapons\big_guns.dm"
+#include "modular_doppler\wargaming\code\unit_weapons\bombs.dm"
+#include "modular_doppler\wargaming\code\unit_weapons\missiles.dm"
+#include "modular_doppler\wargaming\code\unit_weapons\ramming.dm"
+#include "modular_doppler\wargaming\code\unit_weapons\small_guns.dm"
#include "modular_doppler\z_lore_rename\pa_4ca.dm"
#include "modular_doppler\z_lore_rename\syndicate_removal.dm"
// END_INCLUDE