diff --git a/.github/workflows/discord_pr_announce.yml b/.github/workflows/discord_pr_announce.yml index 45ff33fc5013..092e7d69a9d0 100644 --- a/.github/workflows/discord_pr_announce.yml +++ b/.github/workflows/discord_pr_announce.yml @@ -1,7 +1,7 @@ name: "Discord PR Notification" on: pull_request_target: - types: [opened, closed] + types: [opened, closed, reopened] jobs: notify: @@ -19,15 +19,11 @@ jobs: uses: tgstation/discord-notify@main if: > steps.secrets_set.outputs.SECRETS_ENABLED && - (github.event.pull_request.merged == true || github.event.action == 'opened') && + (github.event.pull_request.merged == true || github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.author_association != 'FIRST_TIMER' && github.event.pull_request.author_association != 'FIRST_TIME_CONTRIBUTOR' with: webhook_url: ${{ secrets.DISCORD_WEBHOOK }} - title: ${{ github.event.pull_request.user.login }} - ${{ github.event.pull_request.title }} - message: GET_ACTION include_image: false show_author: false - avatar_url: https://avatars.githubusercontent.com/u/92191611?s=200&v=4 - username: GitHub - title_url: "${{ github.event.pull_request.html_url }}" + avatar_url: https://avatars.githubusercontent.com/u/1363778?s=200&v=4 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a7702b385ee8..42da3c443b23 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -76,5 +76,3 @@ jobs: include_image: false show_author: false avatar_url: https://avatars.githubusercontent.com/u/1363778?s=200&v=4 - username: GitHub - title_url: "${{ matrix.pull_request.html_url }}" diff --git a/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm b/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm index d8ca28b7b004..2b43ae43fb36 100644 --- a/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm +++ b/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm @@ -21404,7 +21404,7 @@ codes_txt = "delivery;dir=4"; location = "Cargo Storage4" }, -/mob/living/simple_animal/bot/mulebot{ +/mob/living/basic/bot/mulebot{ name = "Old Yeller" }, /turf/open/floor/iron/recharge_floor, @@ -33958,7 +33958,7 @@ codes_txt = "delivery;dir=4"; location = "Cargo Storage-2" }, -/mob/living/simple_animal/bot/mulebot{ +/mob/living/basic/bot/mulebot{ name = "Parcel Pete" }, /obj/item/radio/intercom/directional/west, diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm index d45b96d82e92..603d26b7e388 100644 --- a/_maps/map_files/Deltastation/DeltaStation2.dmm +++ b/_maps/map_files/Deltastation/DeltaStation2.dmm @@ -22768,7 +22768,7 @@ name = "warehouse shutters control" }, /obj/structure/window/reinforced/spawner/directional/north, -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/machinery/navbeacon{ codes_txt = "delivery;dir=4"; location = "QM #1" @@ -48596,7 +48596,7 @@ /turf/open/floor/iron, /area/station/maintenance/port/aft) "lUm" = ( -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/machinery/navbeacon{ codes_txt = "delivery;dir=4"; location = "QM #4" diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm index 6fb2cc5a05d5..0e57c80ac1ed 100644 --- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm +++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm @@ -63666,7 +63666,7 @@ }, /obj/effect/turf_decal/bot, /obj/machinery/light/small/directional/east, -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/machinery/status_display/supply{ pixel_x = 32 }, @@ -81448,7 +81448,7 @@ location = "QM #1" }, /obj/effect/turf_decal/bot, -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /turf/open/floor/iron, /area/station/cargo/storage) "xaH" = ( diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm index f24d88e80f5f..48ef10d2056c 100644 --- a/_maps/map_files/MetaStation/MetaStation.dmm +++ b/_maps/map_files/MetaStation/MetaStation.dmm @@ -20096,7 +20096,7 @@ /turf/open/floor/engine, /area/station/engineering/supermatter/room) "gWz" = ( -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/structure/cable, /obj/machinery/navbeacon{ codes_txt = "delivery;dir=1"; @@ -21887,7 +21887,7 @@ /turf/open/floor/iron/dark, /area/station/ai/satellite/exterior) "hBo" = ( -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/structure/cable, /obj/machinery/navbeacon{ codes_txt = "delivery;dir=2"; @@ -30638,7 +30638,7 @@ /obj/effect/decal/cleanable/dirt, /obj/effect/decal/cleanable/blood/oil/slippery, /obj/effect/decal/cleanable/blood/gibs/down, -/mob/living/simple_animal/bot/mulebot{ +/mob/living/basic/bot/mulebot{ name = "Leaping Rabbit" }, /obj/structure/disposalpipe/segment, diff --git a/_maps/map_files/NebulaStation/NebulaStation.dmm b/_maps/map_files/NebulaStation/NebulaStation.dmm index bc1b618e2a9c..5d8e83656d1c 100644 --- a/_maps/map_files/NebulaStation/NebulaStation.dmm +++ b/_maps/map_files/NebulaStation/NebulaStation.dmm @@ -18050,7 +18050,7 @@ /area/station/engineering/storage) "cII" = ( /obj/effect/turf_decal/bot, -/mob/living/simple_animal/bot/mulebot{ +/mob/living/basic/bot/mulebot{ home_destination = "QM #2"; suffix = "#2" }, @@ -75323,7 +75323,7 @@ /area/station/engineering/supermatter/room) "lcm" = ( /obj/effect/turf_decal/bot, -/mob/living/simple_animal/bot/mulebot{ +/mob/living/basic/bot/mulebot{ home_destination = "QM #2"; suffix = "#2" }, @@ -90694,7 +90694,7 @@ /area/station/hallway/primary/fore) "nvZ" = ( /obj/effect/turf_decal/bot, -/mob/living/simple_animal/bot/mulebot{ +/mob/living/basic/bot/mulebot{ home_destination = "QM #2"; suffix = "#2" }, diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm index ae67e403c83a..32ee38f90e4a 100644 --- a/_maps/map_files/tramstation/tramstation.dmm +++ b/_maps/map_files/tramstation/tramstation.dmm @@ -37258,7 +37258,7 @@ /turf/open/floor/iron/dark, /area/station/security/prison/garden) "lQz" = ( -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/effect/turf_decal/delivery, /obj/machinery/navbeacon{ location = "QM #2"; @@ -42313,7 +42313,7 @@ /turf/open/floor/iron/dark, /area/station/medical/virology) "nIC" = ( -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/effect/turf_decal/delivery, /obj/machinery/navbeacon{ location = "QM #6"; @@ -48536,7 +48536,7 @@ /turf/open/floor/iron/dark, /area/station/ai/satellite/interior) "pXq" = ( -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/effect/turf_decal/delivery, /obj/machinery/navbeacon{ location = "QM #3"; @@ -53141,7 +53141,7 @@ /turf/closed/wall, /area/station/engineering/atmospherics_engine) "rCs" = ( -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/effect/turf_decal/delivery, /obj/machinery/navbeacon{ location = "QM #1"; diff --git a/_maps/map_files/wawastation/wawastation.dmm b/_maps/map_files/wawastation/wawastation.dmm index 81ca734b8e21..cac939f11741 100644 --- a/_maps/map_files/wawastation/wawastation.dmm +++ b/_maps/map_files/wawastation/wawastation.dmm @@ -3279,7 +3279,7 @@ codes_txt = "delivery;dir=8"; location = "QM #2" }, -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /turf/open/floor/iron, /area/station/cargo/warehouse) "beO" = ( @@ -16366,7 +16366,7 @@ codes_txt = "delivery;dir=8"; location = "QM #1" }, -/mob/living/simple_animal/bot/mulebot, +/mob/living/basic/bot/mulebot, /obj/machinery/light/small/directional/east, /turf/open/floor/iron, /area/station/cargo/warehouse) diff --git a/_maps/shuttles/arrival_birdshot.dmm b/_maps/shuttles/arrival/arrival_birdshot.dmm similarity index 100% rename from _maps/shuttles/arrival_birdshot.dmm rename to _maps/shuttles/arrival/arrival_birdshot.dmm diff --git a/_maps/shuttles/arrival_box.dmm b/_maps/shuttles/arrival/arrival_box.dmm similarity index 100% rename from _maps/shuttles/arrival_box.dmm rename to _maps/shuttles/arrival/arrival_box.dmm diff --git a/_maps/shuttles/arrival_catwalk.dmm b/_maps/shuttles/arrival/arrival_catwalk.dmm similarity index 100% rename from _maps/shuttles/arrival_catwalk.dmm rename to _maps/shuttles/arrival/arrival_catwalk.dmm diff --git a/_maps/shuttles/arrival_delta.dmm b/_maps/shuttles/arrival/arrival_delta.dmm similarity index 100% rename from _maps/shuttles/arrival_delta.dmm rename to _maps/shuttles/arrival/arrival_delta.dmm diff --git a/_maps/shuttles/arrival_donut.dmm b/_maps/shuttles/arrival/arrival_donut.dmm similarity index 100% rename from _maps/shuttles/arrival_donut.dmm rename to _maps/shuttles/arrival/arrival_donut.dmm diff --git a/_maps/shuttles/arrival_kilo.dmm b/_maps/shuttles/arrival/arrival_kilo.dmm similarity index 100% rename from _maps/shuttles/arrival_kilo.dmm rename to _maps/shuttles/arrival/arrival_kilo.dmm diff --git a/_maps/shuttles/arrival_nebula.dmm b/_maps/shuttles/arrival/arrival_nebula.dmm similarity index 100% rename from _maps/shuttles/arrival_nebula.dmm rename to _maps/shuttles/arrival/arrival_nebula.dmm diff --git a/_maps/shuttles/arrival_northstar.dmm b/_maps/shuttles/arrival/arrival_northstar.dmm similarity index 100% rename from _maps/shuttles/arrival_northstar.dmm rename to _maps/shuttles/arrival/arrival_northstar.dmm diff --git a/_maps/shuttles/arrival_pubby.dmm b/_maps/shuttles/arrival/arrival_pubby.dmm similarity index 100% rename from _maps/shuttles/arrival_pubby.dmm rename to _maps/shuttles/arrival/arrival_pubby.dmm diff --git a/_maps/shuttles/aux_base_default.dmm b/_maps/shuttles/aux_base/aux_base_default.dmm similarity index 100% rename from _maps/shuttles/aux_base_default.dmm rename to _maps/shuttles/aux_base/aux_base_default.dmm diff --git a/_maps/shuttles/aux_base_small.dmm b/_maps/shuttles/aux_base/aux_base_small.dmm similarity index 100% rename from _maps/shuttles/aux_base_small.dmm rename to _maps/shuttles/aux_base/aux_base_small.dmm diff --git a/_maps/shuttles/cargo_birdboat.dmm b/_maps/shuttles/cargo/cargo_birdboat.dmm similarity index 100% rename from _maps/shuttles/cargo_birdboat.dmm rename to _maps/shuttles/cargo/cargo_birdboat.dmm diff --git a/_maps/shuttles/cargo_birdshot.dmm b/_maps/shuttles/cargo/cargo_birdshot.dmm similarity index 100% rename from _maps/shuttles/cargo_birdshot.dmm rename to _maps/shuttles/cargo/cargo_birdshot.dmm diff --git a/_maps/shuttles/cargo_box.dmm b/_maps/shuttles/cargo/cargo_box.dmm similarity index 100% rename from _maps/shuttles/cargo_box.dmm rename to _maps/shuttles/cargo/cargo_box.dmm diff --git a/_maps/shuttles/cargo_catwalk.dmm b/_maps/shuttles/cargo/cargo_catwalk.dmm similarity index 100% rename from _maps/shuttles/cargo_catwalk.dmm rename to _maps/shuttles/cargo/cargo_catwalk.dmm diff --git a/_maps/shuttles/cargo_delta.dmm b/_maps/shuttles/cargo/cargo_delta.dmm similarity index 100% rename from _maps/shuttles/cargo_delta.dmm rename to _maps/shuttles/cargo/cargo_delta.dmm diff --git a/_maps/shuttles/cargo_kilo.dmm b/_maps/shuttles/cargo/cargo_kilo.dmm similarity index 100% rename from _maps/shuttles/cargo_kilo.dmm rename to _maps/shuttles/cargo/cargo_kilo.dmm diff --git a/_maps/shuttles/cargo_mini.dmm b/_maps/shuttles/cargo/cargo_mini.dmm similarity index 100% rename from _maps/shuttles/cargo_mini.dmm rename to _maps/shuttles/cargo/cargo_mini.dmm diff --git a/_maps/shuttles/cargo_nebula.dmm b/_maps/shuttles/cargo/cargo_nebula.dmm similarity index 100% rename from _maps/shuttles/cargo_nebula.dmm rename to _maps/shuttles/cargo/cargo_nebula.dmm diff --git a/_maps/shuttles/cargo_northstar.dmm b/_maps/shuttles/cargo/cargo_northstar.dmm similarity index 100% rename from _maps/shuttles/cargo_northstar.dmm rename to _maps/shuttles/cargo/cargo_northstar.dmm diff --git a/_maps/shuttles/cargo_pubby.dmm b/_maps/shuttles/cargo/cargo_pubby.dmm similarity index 100% rename from _maps/shuttles/cargo_pubby.dmm rename to _maps/shuttles/cargo/cargo_pubby.dmm diff --git a/_maps/shuttles/emergency_arena.dmm b/_maps/shuttles/emergency/emergency_arena.dmm similarity index 100% rename from _maps/shuttles/emergency_arena.dmm rename to _maps/shuttles/emergency/emergency_arena.dmm diff --git a/_maps/shuttles/emergency_asteroid.dmm b/_maps/shuttles/emergency/emergency_asteroid.dmm similarity index 100% rename from _maps/shuttles/emergency_asteroid.dmm rename to _maps/shuttles/emergency/emergency_asteroid.dmm diff --git a/_maps/shuttles/emergency_backup.dmm b/_maps/shuttles/emergency/emergency_backup.dmm similarity index 100% rename from _maps/shuttles/emergency_backup.dmm rename to _maps/shuttles/emergency/emergency_backup.dmm diff --git a/_maps/shuttles/emergency_bar.dmm b/_maps/shuttles/emergency/emergency_bar.dmm similarity index 100% rename from _maps/shuttles/emergency_bar.dmm rename to _maps/shuttles/emergency/emergency_bar.dmm diff --git a/_maps/shuttles/emergency_bballhooper.dmm b/_maps/shuttles/emergency/emergency_bballhooper.dmm similarity index 100% rename from _maps/shuttles/emergency_bballhooper.dmm rename to _maps/shuttles/emergency/emergency_bballhooper.dmm diff --git a/_maps/shuttles/emergency_birdboat.dmm b/_maps/shuttles/emergency/emergency_birdboat.dmm similarity index 100% rename from _maps/shuttles/emergency_birdboat.dmm rename to _maps/shuttles/emergency/emergency_birdboat.dmm diff --git a/_maps/shuttles/emergency_birdshot.dmm b/_maps/shuttles/emergency/emergency_birdshot.dmm similarity index 100% rename from _maps/shuttles/emergency_birdshot.dmm rename to _maps/shuttles/emergency/emergency_birdshot.dmm diff --git a/_maps/shuttles/emergency_box.dmm b/_maps/shuttles/emergency/emergency_box.dmm similarity index 100% rename from _maps/shuttles/emergency_box.dmm rename to _maps/shuttles/emergency/emergency_box.dmm diff --git a/_maps/shuttles/emergency_casino.dmm b/_maps/shuttles/emergency/emergency_casino.dmm similarity index 100% rename from _maps/shuttles/emergency_casino.dmm rename to _maps/shuttles/emergency/emergency_casino.dmm diff --git a/_maps/shuttles/emergency_catwalk.dmm b/_maps/shuttles/emergency/emergency_catwalk.dmm similarity index 100% rename from _maps/shuttles/emergency_catwalk.dmm rename to _maps/shuttles/emergency/emergency_catwalk.dmm diff --git a/_maps/shuttles/emergency_cere.dmm b/_maps/shuttles/emergency/emergency_cere.dmm similarity index 100% rename from _maps/shuttles/emergency_cere.dmm rename to _maps/shuttles/emergency/emergency_cere.dmm diff --git a/_maps/shuttles/emergency_clown.dmm b/_maps/shuttles/emergency/emergency_clown.dmm similarity index 100% rename from _maps/shuttles/emergency_clown.dmm rename to _maps/shuttles/emergency/emergency_clown.dmm diff --git a/_maps/shuttles/emergency_construction.dmm b/_maps/shuttles/emergency/emergency_construction.dmm similarity index 100% rename from _maps/shuttles/emergency_construction.dmm rename to _maps/shuttles/emergency/emergency_construction.dmm diff --git a/_maps/shuttles/emergency_constructionbig.dmm b/_maps/shuttles/emergency/emergency_constructionbig.dmm similarity index 100% rename from _maps/shuttles/emergency_constructionbig.dmm rename to _maps/shuttles/emergency/emergency_constructionbig.dmm diff --git a/_maps/shuttles/emergency_cramped.dmm b/_maps/shuttles/emergency/emergency_cramped.dmm similarity index 100% rename from _maps/shuttles/emergency_cramped.dmm rename to _maps/shuttles/emergency/emergency_cramped.dmm diff --git a/_maps/shuttles/emergency_cruise.dmm b/_maps/shuttles/emergency/emergency_cruise.dmm similarity index 100% rename from _maps/shuttles/emergency_cruise.dmm rename to _maps/shuttles/emergency/emergency_cruise.dmm diff --git a/_maps/shuttles/emergency_delta.dmm b/_maps/shuttles/emergency/emergency_delta.dmm similarity index 100% rename from _maps/shuttles/emergency_delta.dmm rename to _maps/shuttles/emergency/emergency_delta.dmm diff --git a/_maps/shuttles/emergency_discoinferno.dmm b/_maps/shuttles/emergency/emergency_discoinferno.dmm similarity index 100% rename from _maps/shuttles/emergency_discoinferno.dmm rename to _maps/shuttles/emergency/emergency_discoinferno.dmm diff --git a/_maps/shuttles/emergency_donut.dmm b/_maps/shuttles/emergency/emergency_donut.dmm similarity index 100% rename from _maps/shuttles/emergency_donut.dmm rename to _maps/shuttles/emergency/emergency_donut.dmm diff --git a/_maps/shuttles/emergency_fame.dmm b/_maps/shuttles/emergency/emergency_fame.dmm similarity index 100% rename from _maps/shuttles/emergency_fame.dmm rename to _maps/shuttles/emergency/emergency_fame.dmm diff --git a/_maps/shuttles/emergency_fish.dmm b/_maps/shuttles/emergency/emergency_fish.dmm similarity index 100% rename from _maps/shuttles/emergency_fish.dmm rename to _maps/shuttles/emergency/emergency_fish.dmm diff --git a/_maps/shuttles/emergency_goon.dmm b/_maps/shuttles/emergency/emergency_goon.dmm similarity index 100% rename from _maps/shuttles/emergency_goon.dmm rename to _maps/shuttles/emergency/emergency_goon.dmm diff --git a/_maps/shuttles/emergency_hugcage.dmm b/_maps/shuttles/emergency/emergency_hugcage.dmm similarity index 100% rename from _maps/shuttles/emergency_hugcage.dmm rename to _maps/shuttles/emergency/emergency_hugcage.dmm diff --git a/_maps/shuttles/emergency_humpback.dmm b/_maps/shuttles/emergency/emergency_humpback.dmm similarity index 100% rename from _maps/shuttles/emergency_humpback.dmm rename to _maps/shuttles/emergency/emergency_humpback.dmm diff --git a/_maps/shuttles/emergency_imfedupwiththisworld.dmm b/_maps/shuttles/emergency/emergency_imfedupwiththisworld.dmm similarity index 100% rename from _maps/shuttles/emergency_imfedupwiththisworld.dmm rename to _maps/shuttles/emergency/emergency_imfedupwiththisworld.dmm diff --git a/_maps/shuttles/emergency_kilo.dmm b/_maps/shuttles/emergency/emergency_kilo.dmm similarity index 100% rename from _maps/shuttles/emergency_kilo.dmm rename to _maps/shuttles/emergency/emergency_kilo.dmm diff --git a/_maps/shuttles/emergency_lance.dmm b/_maps/shuttles/emergency/emergency_lance.dmm similarity index 100% rename from _maps/shuttles/emergency_lance.dmm rename to _maps/shuttles/emergency/emergency_lance.dmm diff --git a/_maps/shuttles/emergency_luxury.dmm b/_maps/shuttles/emergency/emergency_luxury.dmm similarity index 100% rename from _maps/shuttles/emergency_luxury.dmm rename to _maps/shuttles/emergency/emergency_luxury.dmm diff --git a/_maps/shuttles/emergency_medisim.dmm b/_maps/shuttles/emergency/emergency_medisim.dmm similarity index 100% rename from _maps/shuttles/emergency_medisim.dmm rename to _maps/shuttles/emergency/emergency_medisim.dmm diff --git a/_maps/shuttles/emergency_meta.dmm b/_maps/shuttles/emergency/emergency_meta.dmm similarity index 100% rename from _maps/shuttles/emergency_meta.dmm rename to _maps/shuttles/emergency/emergency_meta.dmm diff --git a/_maps/shuttles/emergency_meteor.dmm b/_maps/shuttles/emergency/emergency_meteor.dmm similarity index 100% rename from _maps/shuttles/emergency_meteor.dmm rename to _maps/shuttles/emergency/emergency_meteor.dmm diff --git a/_maps/shuttles/emergency_mini.dmm b/_maps/shuttles/emergency/emergency_mini.dmm similarity index 100% rename from _maps/shuttles/emergency_mini.dmm rename to _maps/shuttles/emergency/emergency_mini.dmm diff --git a/_maps/shuttles/emergency_monastery.dmm b/_maps/shuttles/emergency/emergency_monastery.dmm similarity index 100% rename from _maps/shuttles/emergency_monastery.dmm rename to _maps/shuttles/emergency/emergency_monastery.dmm diff --git a/_maps/shuttles/emergency_narnar.dmm b/_maps/shuttles/emergency/emergency_narnar.dmm similarity index 100% rename from _maps/shuttles/emergency_narnar.dmm rename to _maps/shuttles/emergency/emergency_narnar.dmm diff --git a/_maps/shuttles/emergency_nature.dmm b/_maps/shuttles/emergency/emergency_nature.dmm similarity index 100% rename from _maps/shuttles/emergency_nature.dmm rename to _maps/shuttles/emergency/emergency_nature.dmm diff --git a/_maps/shuttles/emergency_nebula.dmm b/_maps/shuttles/emergency/emergency_nebula.dmm similarity index 100% rename from _maps/shuttles/emergency_nebula.dmm rename to _maps/shuttles/emergency/emergency_nebula.dmm diff --git a/_maps/shuttles/emergency_northstar.dmm b/_maps/shuttles/emergency/emergency_northstar.dmm similarity index 100% rename from _maps/shuttles/emergency_northstar.dmm rename to _maps/shuttles/emergency/emergency_northstar.dmm diff --git a/_maps/shuttles/emergency_omega.dmm b/_maps/shuttles/emergency/emergency_omega.dmm similarity index 100% rename from _maps/shuttles/emergency_omega.dmm rename to _maps/shuttles/emergency/emergency_omega.dmm diff --git a/_maps/shuttles/emergency_pod.dmm b/_maps/shuttles/emergency/emergency_pod.dmm similarity index 100% rename from _maps/shuttles/emergency_pod.dmm rename to _maps/shuttles/emergency/emergency_pod.dmm diff --git a/_maps/shuttles/emergency_pubby.dmm b/_maps/shuttles/emergency/emergency_pubby.dmm similarity index 100% rename from _maps/shuttles/emergency_pubby.dmm rename to _maps/shuttles/emergency/emergency_pubby.dmm diff --git a/_maps/shuttles/emergency_raven.dmm b/_maps/shuttles/emergency/emergency_raven.dmm similarity index 100% rename from _maps/shuttles/emergency_raven.dmm rename to _maps/shuttles/emergency/emergency_raven.dmm diff --git a/_maps/shuttles/emergency_rollerdome.dmm b/_maps/shuttles/emergency/emergency_rollerdome.dmm similarity index 100% rename from _maps/shuttles/emergency_rollerdome.dmm rename to _maps/shuttles/emergency/emergency_rollerdome.dmm diff --git a/_maps/shuttles/emergency_russiafightpit.dmm b/_maps/shuttles/emergency/emergency_russiafightpit.dmm similarity index 100% rename from _maps/shuttles/emergency_russiafightpit.dmm rename to _maps/shuttles/emergency/emergency_russiafightpit.dmm diff --git a/_maps/shuttles/emergency_scrapheap.dmm b/_maps/shuttles/emergency/emergency_scrapheap.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap.dmm rename to _maps/shuttles/emergency/emergency_scrapheap.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic1.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic1.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic1.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic1.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic2.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic2.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic2.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic2.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_aft1.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_aft1.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_aft1.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_aft1.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_aft2.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_aft2.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_aft2.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_aft2.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_aft3.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_aft3.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_aft3.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_aft3.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_fore1.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_fore1.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_fore1.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_fore1.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_fore2.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_fore2.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_fore2.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_fore2.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_fore3.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_fore3.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_fore3.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_fore3.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_starboard1.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_starboard1.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_starboard1.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_starboard1.dmm diff --git a/_maps/shuttles/emergency_scrapheap/classic_starboard2.dmm b/_maps/shuttles/emergency/emergency_scrapheap/classic_starboard2.dmm similarity index 100% rename from _maps/shuttles/emergency_scrapheap/classic_starboard2.dmm rename to _maps/shuttles/emergency/emergency_scrapheap/classic_starboard2.dmm diff --git a/_maps/shuttles/emergency_shadow.dmm b/_maps/shuttles/emergency/emergency_shadow.dmm similarity index 100% rename from _maps/shuttles/emergency_shadow.dmm rename to _maps/shuttles/emergency/emergency_shadow.dmm diff --git a/_maps/shuttles/emergency_supermatter.dmm b/_maps/shuttles/emergency/emergency_supermatter.dmm similarity index 100% rename from _maps/shuttles/emergency_supermatter.dmm rename to _maps/shuttles/emergency/emergency_supermatter.dmm diff --git a/_maps/shuttles/emergency_tram.dmm b/_maps/shuttles/emergency/emergency_tram.dmm similarity index 100% rename from _maps/shuttles/emergency_tram.dmm rename to _maps/shuttles/emergency/emergency_tram.dmm diff --git a/_maps/shuttles/emergency_tranquility.dmm b/_maps/shuttles/emergency/emergency_tranquility.dmm similarity index 100% rename from _maps/shuttles/emergency_tranquility.dmm rename to _maps/shuttles/emergency/emergency_tranquility.dmm diff --git a/_maps/shuttles/emergency_venture.dmm b/_maps/shuttles/emergency/emergency_venture.dmm similarity index 100% rename from _maps/shuttles/emergency_venture.dmm rename to _maps/shuttles/emergency/emergency_venture.dmm diff --git a/_maps/shuttles/emergency_wabbajack.dmm b/_maps/shuttles/emergency/emergency_wabbajack.dmm similarity index 100% rename from _maps/shuttles/emergency_wabbajack.dmm rename to _maps/shuttles/emergency/emergency_wabbajack.dmm diff --git a/_maps/shuttles/emergency_wawa.dmm b/_maps/shuttles/emergency/emergency_wawa.dmm similarity index 100% rename from _maps/shuttles/emergency_wawa.dmm rename to _maps/shuttles/emergency/emergency_wawa.dmm diff --git a/_maps/shuttles/emergency_zeta.dmm b/_maps/shuttles/emergency/emergency_zeta.dmm similarity index 100% rename from _maps/shuttles/emergency_zeta.dmm rename to _maps/shuttles/emergency/emergency_zeta.dmm diff --git a/_maps/shuttles/escape_pod_cramped.dmm b/_maps/shuttles/escape_pod/escape_pod_cramped.dmm similarity index 100% rename from _maps/shuttles/escape_pod_cramped.dmm rename to _maps/shuttles/escape_pod/escape_pod_cramped.dmm diff --git a/_maps/shuttles/escape_pod_default.dmm b/_maps/shuttles/escape_pod/escape_pod_default.dmm similarity index 100% rename from _maps/shuttles/escape_pod_default.dmm rename to _maps/shuttles/escape_pod/escape_pod_default.dmm diff --git a/_maps/shuttles/escape_pod_large.dmm b/_maps/shuttles/escape_pod/escape_pod_large.dmm similarity index 100% rename from _maps/shuttles/escape_pod_large.dmm rename to _maps/shuttles/escape_pod/escape_pod_large.dmm diff --git a/_maps/shuttles/escape_pod_luxury.dmm b/_maps/shuttles/escape_pod/escape_pod_luxury.dmm similarity index 100% rename from _maps/shuttles/escape_pod_luxury.dmm rename to _maps/shuttles/escape_pod/escape_pod_luxury.dmm diff --git a/_maps/shuttles/ferry_base.dmm b/_maps/shuttles/ferry/ferry_base.dmm similarity index 100% rename from _maps/shuttles/ferry_base.dmm rename to _maps/shuttles/ferry/ferry_base.dmm diff --git a/_maps/shuttles/ferry_fancy.dmm b/_maps/shuttles/ferry/ferry_fancy.dmm similarity index 100% rename from _maps/shuttles/ferry_fancy.dmm rename to _maps/shuttles/ferry/ferry_fancy.dmm diff --git a/_maps/shuttles/ferry_kilo.dmm b/_maps/shuttles/ferry/ferry_kilo.dmm similarity index 100% rename from _maps/shuttles/ferry_kilo.dmm rename to _maps/shuttles/ferry/ferry_kilo.dmm diff --git a/_maps/shuttles/ferry_lighthouse.dmm b/_maps/shuttles/ferry/ferry_lighthouse.dmm similarity index 100% rename from _maps/shuttles/ferry_lighthouse.dmm rename to _maps/shuttles/ferry/ferry_lighthouse.dmm diff --git a/_maps/shuttles/ferry_meat.dmm b/_maps/shuttles/ferry/ferry_meat.dmm similarity index 100% rename from _maps/shuttles/ferry_meat.dmm rename to _maps/shuttles/ferry/ferry_meat.dmm diff --git a/_maps/shuttles/ferry_nebula.dmm b/_maps/shuttles/ferry/ferry_nebula.dmm similarity index 100% rename from _maps/shuttles/ferry_nebula.dmm rename to _maps/shuttles/ferry/ferry_nebula.dmm diff --git a/_maps/shuttles/hunter_bounty.dmm b/_maps/shuttles/hunter/hunter_bounty.dmm similarity index 100% rename from _maps/shuttles/hunter_bounty.dmm rename to _maps/shuttles/hunter/hunter_bounty.dmm diff --git a/_maps/shuttles/hunter_mi13_foodtruck.dmm b/_maps/shuttles/hunter/hunter_mi13_foodtruck.dmm similarity index 100% rename from _maps/shuttles/hunter_mi13_foodtruck.dmm rename to _maps/shuttles/hunter/hunter_mi13_foodtruck.dmm diff --git a/_maps/shuttles/hunter_psyker.dmm b/_maps/shuttles/hunter/hunter_psyker.dmm similarity index 100% rename from _maps/shuttles/hunter_psyker.dmm rename to _maps/shuttles/hunter/hunter_psyker.dmm diff --git a/_maps/shuttles/hunter_russian.dmm b/_maps/shuttles/hunter/hunter_russian.dmm similarity index 100% rename from _maps/shuttles/hunter_russian.dmm rename to _maps/shuttles/hunter/hunter_russian.dmm diff --git a/_maps/shuttles/hunter_space_cop.dmm b/_maps/shuttles/hunter/hunter_space_cop.dmm similarity index 100% rename from _maps/shuttles/hunter_space_cop.dmm rename to _maps/shuttles/hunter/hunter_space_cop.dmm diff --git a/_maps/shuttles/infiltrator_advanced.dmm b/_maps/shuttles/infiltrator/infiltrator_advanced.dmm similarity index 100% rename from _maps/shuttles/infiltrator_advanced.dmm rename to _maps/shuttles/infiltrator/infiltrator_advanced.dmm diff --git a/_maps/shuttles/infiltrator_basic.dmm b/_maps/shuttles/infiltrator/infiltrator_basic.dmm similarity index 100% rename from _maps/shuttles/infiltrator_basic.dmm rename to _maps/shuttles/infiltrator/infiltrator_basic.dmm diff --git a/_maps/shuttles/infiltrator_clown.dmm b/_maps/shuttles/infiltrator/infiltrator_clown.dmm similarity index 100% rename from _maps/shuttles/infiltrator_clown.dmm rename to _maps/shuttles/infiltrator/infiltrator_clown.dmm diff --git a/_maps/shuttles/labour_box.dmm b/_maps/shuttles/labour/labour_box.dmm similarity index 100% rename from _maps/shuttles/labour_box.dmm rename to _maps/shuttles/labour/labour_box.dmm diff --git a/_maps/shuttles/labour_delta.dmm b/_maps/shuttles/labour/labour_delta.dmm similarity index 100% rename from _maps/shuttles/labour_delta.dmm rename to _maps/shuttles/labour/labour_delta.dmm diff --git a/_maps/shuttles/labour_generic.dmm b/_maps/shuttles/labour/labour_generic.dmm similarity index 100% rename from _maps/shuttles/labour_generic.dmm rename to _maps/shuttles/labour/labour_generic.dmm diff --git a/_maps/shuttles/labour_kilo.dmm b/_maps/shuttles/labour/labour_kilo.dmm similarity index 100% rename from _maps/shuttles/labour_kilo.dmm rename to _maps/shuttles/labour/labour_kilo.dmm diff --git a/_maps/shuttles/labour_nebula.dmm b/_maps/shuttles/labour/labour_nebula.dmm similarity index 100% rename from _maps/shuttles/labour_nebula.dmm rename to _maps/shuttles/labour/labour_nebula.dmm diff --git a/_maps/shuttles/mining_box.dmm b/_maps/shuttles/mining/mining_box.dmm similarity index 100% rename from _maps/shuttles/mining_box.dmm rename to _maps/shuttles/mining/mining_box.dmm diff --git a/_maps/shuttles/mining_common_kilo.dmm b/_maps/shuttles/mining/mining_common_kilo.dmm similarity index 100% rename from _maps/shuttles/mining_common_kilo.dmm rename to _maps/shuttles/mining/mining_common_kilo.dmm diff --git a/_maps/shuttles/mining_common_meta.dmm b/_maps/shuttles/mining/mining_common_meta.dmm similarity index 100% rename from _maps/shuttles/mining_common_meta.dmm rename to _maps/shuttles/mining/mining_common_meta.dmm diff --git a/_maps/shuttles/mining_common_northstar.dmm b/_maps/shuttles/mining/mining_common_northstar.dmm similarity index 100% rename from _maps/shuttles/mining_common_northstar.dmm rename to _maps/shuttles/mining/mining_common_northstar.dmm diff --git a/_maps/shuttles/mining_delta.dmm b/_maps/shuttles/mining/mining_delta.dmm similarity index 100% rename from _maps/shuttles/mining_delta.dmm rename to _maps/shuttles/mining/mining_delta.dmm diff --git a/_maps/shuttles/mining_freight.dmm b/_maps/shuttles/mining/mining_freight.dmm similarity index 100% rename from _maps/shuttles/mining_freight.dmm rename to _maps/shuttles/mining/mining_freight.dmm diff --git a/_maps/shuttles/mining_kilo.dmm b/_maps/shuttles/mining/mining_kilo.dmm similarity index 100% rename from _maps/shuttles/mining_kilo.dmm rename to _maps/shuttles/mining/mining_kilo.dmm diff --git a/_maps/shuttles/mining_large.dmm b/_maps/shuttles/mining/mining_large.dmm similarity index 100% rename from _maps/shuttles/mining_large.dmm rename to _maps/shuttles/mining/mining_large.dmm diff --git a/_maps/shuttles/mining_nebula.dmm b/_maps/shuttles/mining/mining_nebula.dmm similarity index 100% rename from _maps/shuttles/mining_nebula.dmm rename to _maps/shuttles/mining/mining_nebula.dmm diff --git a/_maps/shuttles/mining_northstar.dmm b/_maps/shuttles/mining/mining_northstar.dmm similarity index 100% rename from _maps/shuttles/mining_northstar.dmm rename to _maps/shuttles/mining/mining_northstar.dmm diff --git a/_maps/shuttles/pirate_default.dmm b/_maps/shuttles/pirate/pirate_default.dmm similarity index 100% rename from _maps/shuttles/pirate_default.dmm rename to _maps/shuttles/pirate/pirate_default.dmm diff --git a/_maps/shuttles/pirate_dutchman.dmm b/_maps/shuttles/pirate/pirate_dutchman.dmm similarity index 100% rename from _maps/shuttles/pirate_dutchman.dmm rename to _maps/shuttles/pirate/pirate_dutchman.dmm diff --git a/_maps/shuttles/pirate_ex_interdyne.dmm b/_maps/shuttles/pirate/pirate_ex_interdyne.dmm similarity index 100% rename from _maps/shuttles/pirate_ex_interdyne.dmm rename to _maps/shuttles/pirate/pirate_ex_interdyne.dmm diff --git a/_maps/shuttles/pirate_geode.dmm b/_maps/shuttles/pirate/pirate_geode.dmm similarity index 100% rename from _maps/shuttles/pirate_geode.dmm rename to _maps/shuttles/pirate/pirate_geode.dmm diff --git a/_maps/shuttles/pirate_grey.dmm b/_maps/shuttles/pirate/pirate_grey.dmm similarity index 100% rename from _maps/shuttles/pirate_grey.dmm rename to _maps/shuttles/pirate/pirate_grey.dmm diff --git a/_maps/shuttles/pirate_irs.dmm b/_maps/shuttles/pirate/pirate_irs.dmm similarity index 100% rename from _maps/shuttles/pirate_irs.dmm rename to _maps/shuttles/pirate/pirate_irs.dmm diff --git a/_maps/shuttles/pirate_medieval.dmm b/_maps/shuttles/pirate/pirate_medieval.dmm similarity index 100% rename from _maps/shuttles/pirate_medieval.dmm rename to _maps/shuttles/pirate/pirate_medieval.dmm diff --git a/_maps/shuttles/pirate_silverscale.dmm b/_maps/shuttles/pirate/pirate_silverscale.dmm similarity index 100% rename from _maps/shuttles/pirate_silverscale.dmm rename to _maps/shuttles/pirate/pirate_silverscale.dmm diff --git a/_maps/shuttles/ruin_caravan_victim.dmm b/_maps/shuttles/ruin/ruin_caravan_victim.dmm similarity index 100% rename from _maps/shuttles/ruin_caravan_victim.dmm rename to _maps/shuttles/ruin/ruin_caravan_victim.dmm diff --git a/_maps/shuttles/ruin_cyborg_mothership.dmm b/_maps/shuttles/ruin/ruin_cyborg_mothership.dmm similarity index 100% rename from _maps/shuttles/ruin_cyborg_mothership.dmm rename to _maps/shuttles/ruin/ruin_cyborg_mothership.dmm diff --git a/_maps/shuttles/ruin_pirate_cutter.dmm b/_maps/shuttles/ruin/ruin_pirate_cutter.dmm similarity index 100% rename from _maps/shuttles/ruin_pirate_cutter.dmm rename to _maps/shuttles/ruin/ruin_pirate_cutter.dmm diff --git a/_maps/shuttles/ruin_syndicate_dropship.dmm b/_maps/shuttles/ruin/ruin_syndicate_dropship.dmm similarity index 100% rename from _maps/shuttles/ruin_syndicate_dropship.dmm rename to _maps/shuttles/ruin/ruin_syndicate_dropship.dmm diff --git a/_maps/shuttles/ruin_syndicate_fighter_shiv.dmm b/_maps/shuttles/ruin/ruin_syndicate_fighter_shiv.dmm similarity index 100% rename from _maps/shuttles/ruin_syndicate_fighter_shiv.dmm rename to _maps/shuttles/ruin/ruin_syndicate_fighter_shiv.dmm diff --git a/_maps/shuttles/starfury_corvette.dmm b/_maps/shuttles/starfury/starfury_corvette.dmm similarity index 100% rename from _maps/shuttles/starfury_corvette.dmm rename to _maps/shuttles/starfury/starfury_corvette.dmm diff --git a/_maps/shuttles/starfury_fighter1.dmm b/_maps/shuttles/starfury/starfury_fighter1.dmm similarity index 100% rename from _maps/shuttles/starfury_fighter1.dmm rename to _maps/shuttles/starfury/starfury_fighter1.dmm diff --git a/_maps/shuttles/starfury_fighter2.dmm b/_maps/shuttles/starfury/starfury_fighter2.dmm similarity index 100% rename from _maps/shuttles/starfury_fighter2.dmm rename to _maps/shuttles/starfury/starfury_fighter2.dmm diff --git a/_maps/shuttles/starfury_fighter3.dmm b/_maps/shuttles/starfury/starfury_fighter3.dmm similarity index 100% rename from _maps/shuttles/starfury_fighter3.dmm rename to _maps/shuttles/starfury/starfury_fighter3.dmm diff --git a/_maps/shuttles/whiteship_birdshot.dmm b/_maps/shuttles/whiteship/whiteship_birdshot.dmm similarity index 100% rename from _maps/shuttles/whiteship_birdshot.dmm rename to _maps/shuttles/whiteship/whiteship_birdshot.dmm diff --git a/_maps/shuttles/whiteship_box.dmm b/_maps/shuttles/whiteship/whiteship_box.dmm similarity index 100% rename from _maps/shuttles/whiteship_box.dmm rename to _maps/shuttles/whiteship/whiteship_box.dmm diff --git a/_maps/shuttles/whiteship_cere.dmm b/_maps/shuttles/whiteship/whiteship_cere.dmm similarity index 100% rename from _maps/shuttles/whiteship_cere.dmm rename to _maps/shuttles/whiteship/whiteship_cere.dmm diff --git a/_maps/shuttles/whiteship_delta.dmm b/_maps/shuttles/whiteship/whiteship_delta.dmm similarity index 100% rename from _maps/shuttles/whiteship_delta.dmm rename to _maps/shuttles/whiteship/whiteship_delta.dmm diff --git a/_maps/shuttles/whiteship_donut.dmm b/_maps/shuttles/whiteship/whiteship_donut.dmm similarity index 100% rename from _maps/shuttles/whiteship_donut.dmm rename to _maps/shuttles/whiteship/whiteship_donut.dmm diff --git a/_maps/shuttles/whiteship_kilo.dmm b/_maps/shuttles/whiteship/whiteship_kilo.dmm similarity index 100% rename from _maps/shuttles/whiteship_kilo.dmm rename to _maps/shuttles/whiteship/whiteship_kilo.dmm diff --git a/_maps/shuttles/whiteship_meta.dmm b/_maps/shuttles/whiteship/whiteship_meta.dmm similarity index 100% rename from _maps/shuttles/whiteship_meta.dmm rename to _maps/shuttles/whiteship/whiteship_meta.dmm diff --git a/_maps/shuttles/whiteship_obelisk.dmm b/_maps/shuttles/whiteship/whiteship_obelisk.dmm similarity index 100% rename from _maps/shuttles/whiteship_obelisk.dmm rename to _maps/shuttles/whiteship/whiteship_obelisk.dmm diff --git a/_maps/shuttles/whiteship_personalshuttle.dmm b/_maps/shuttles/whiteship/whiteship_personalshuttle.dmm similarity index 100% rename from _maps/shuttles/whiteship_personalshuttle.dmm rename to _maps/shuttles/whiteship/whiteship_personalshuttle.dmm diff --git a/_maps/shuttles/whiteship_pubby.dmm b/_maps/shuttles/whiteship/whiteship_pubby.dmm similarity index 100% rename from _maps/shuttles/whiteship_pubby.dmm rename to _maps/shuttles/whiteship/whiteship_pubby.dmm diff --git a/_maps/shuttles/whiteship_tram.dmm b/_maps/shuttles/whiteship/whiteship_tram.dmm similarity index 100% rename from _maps/shuttles/whiteship_tram.dmm rename to _maps/shuttles/whiteship/whiteship_tram.dmm diff --git a/code/__DEFINES/ai/ai.dm b/code/__DEFINES/ai/ai.dm index f8285b075e4b..a6724aeb4aae 100644 --- a/code/__DEFINES/ai/ai.dm +++ b/code/__DEFINES/ai/ai.dm @@ -21,6 +21,7 @@ ///For JPS pathing, the maximum length of a path we'll try to generate. Should be modularized depending on what we're doing later on #define AI_MAX_PATH_LENGTH 30 // 30 is possibly overkill since by default we lose interest after 14 tiles of distance, but this gives wiggle room for weaving around obstacles #define AI_BOT_PATH_LENGTH 60 +#define AI_MULEBOT_PATH_LENGTH 150 //we making a pilgramage sometimes... // How far should we, by default, be looking for interesting things to de-idle? #define AI_DEFAULT_INTERESTING_DIST 10 diff --git a/code/__DEFINES/ai/bot_keys.dm b/code/__DEFINES/ai/bot_keys.dm index 0c7758f61348..653bace09d8a 100644 --- a/code/__DEFINES/ai/bot_keys.dm +++ b/code/__DEFINES/ai/bot_keys.dm @@ -149,3 +149,12 @@ #define BB_ROBOT_TARGET "robot_target" ///key that holds materials we can refill #define BB_REFILLABLE_TARGET "refillable_target" + + +//mulebots +///key that holds our delivery destination's name +#define BB_MULEBOT_DESTINATION_BEACON "mulebot_destination" +///key that holds our home port's name +#define BB_MULEBOT_HOME_BEACON "mulebot_home_beacon" +///key that holds our current delivery target atom +#define BB_MULEBOT_TRAVEL_TARGET "mulebot_travel_target" diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm index 81c37f60afa5..f706e6b9fe9f 100644 --- a/code/__DEFINES/dcs/signals/signals_global.dm +++ b/code/__DEFINES/dcs/signals/signals_global.dm @@ -88,6 +88,14 @@ /// global signal when a global nullrod type is picked #define COMSIG_GLOB_NULLROD_PICKED "!nullrod_picked" +/// global signal when someone prays via prayer verb. +#define COMSIG_GLOB_SEND_PRAYER "!send_prayer" + #define ARG_PRAYING_MOB 1 + #define ARG_PRAYER_MSG 2 + #define ARG_PRAYER_TYPE 3 + #define ARG_PRAYER_SYMBOL 4 + #define ARG_PRAYED_DEITIES 5 + /// Global signal when light debugging is canceled #define COMSIG_LIGHT_DEBUG_DISABLED "!light_debug_disabled" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm index 7d4707edc06a..dd10ff0e4509 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm @@ -23,11 +23,11 @@ #define COMSIG_MOB_CANCEL_CLICKON (1<<0) ///from base of mob/alt_click_on_secodary(): (atom/A) #define COMSIG_MOB_ALTCLICKON_SECONDARY "mob_altclickon_secondary" -/// From base of /mob/living/simple_animal/bot/proc/bot_step() +/// From base of /mob/living/basic/bot/proc/bot_step() #define COMSIG_MOB_BOT_PRE_STEP "mob_bot_pre_step" /// Should always match COMPONENT_MOVABLE_BLOCK_PRE_MOVE as these are interchangeable and used to block movement. #define COMPONENT_MOB_BOT_BLOCK_PRE_STEP COMPONENT_MOVABLE_BLOCK_PRE_MOVE -/// From base of /mob/living/simple_animal/bot/proc/bot_step() +/// From base of /mob/living/basic/bot/proc/bot_step() #define COMSIG_MOB_BOT_STEP "mob_bot_step" /// From base of /mob/proc/update_held_items diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm index 5fab93418bb7..ca40ca7aef0f 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm @@ -12,5 +12,3 @@ #define COMSIG_SILICON_AI_VACATE_APC "AI_vacate_apc" ///called when an AI's control is toggled #define COMSIG_SILICON_AI_SET_CONTROL_DISABLED "AI_set_control_disabled" -///called when borg module is activated/deactivated -#define COMSIG_SILICON_MODULE_ACTIVATION "borg_module_select" diff --git a/code/__DEFINES/dcs/signals/signals_moveloop.dm b/code/__DEFINES/dcs/signals/signals_moveloop.dm index 8a354f8bfbb1..95208dd1e811 100644 --- a/code/__DEFINES/dcs/signals/signals_moveloop.dm +++ b/code/__DEFINES/dcs/signals/signals_moveloop.dm @@ -11,3 +11,6 @@ #define COMSIG_MOVELOOP_JPS_REPATH "moveloop_jps_repath" ///from [/datum/move_loop/has_target/jps/on_finish_pathing] #define COMSIG_MOVELOOP_JPS_FINISHED_PATHING "moveloop_jps_finished_pathing" + +///from /datum/move_loop/has_target/jps/frustrations/handle_move_attempt_failure +#define COMSIG_MOVELOOP_JPS_FRUSTRATION_INCREMENTED "moveloop_jps_frustration_incremented" diff --git a/code/__DEFINES/gizmo.dm b/code/__DEFINES/gizmo.dm index e48b14e21c28..e8c199741f8c 100644 --- a/code/__DEFINES/gizmo.dm +++ b/code/__DEFINES/gizmo.dm @@ -37,4 +37,5 @@ /datum/gizmodes/sputter = 1,\ /datum/gizmodes/copier = 1,\ /datum/gizmodes/bad = 1,\ + /datum/gizmodes/code_crack/tutorial = 1,\ ) diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm index 1b71b003872b..4e506977dc33 100644 --- a/code/__DEFINES/hud.dm +++ b/code/__DEFINES/hud.dm @@ -158,6 +158,8 @@ #define HUD_WIZARD_COMPACT_PERKS "wizard_compact_perks" #define HUD_WIZARD_PERK(slot) "wizard_perk:[slot]" +#define HUD_MULEBOT_CHARGE "mulebot_charge" + /// Converts item slots to hud keys #define HUD_KEY_ITEM_SLOT(slot) "item_slot:[slot]" /// Converts item slots to hud keys as a compiler constant diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 28300319502b..d314074dc32a 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -192,9 +192,7 @@ GLOBAL_LIST_INIT(turfs_pass_meteor, typecacheof(list( #define isrevenant(A) (istype(A, /mob/living/basic/revenant)) -#define isbot(A) (istype(A, /mob/living/simple_animal/bot) || istype(A, /mob/living/basic/bot)) - -#define isbasicbot(A) (istype(A, /mob/living/basic/bot)) +#define isbot(A) istype(A, /mob/living/basic/bot) #define ismouse(A) (istype(A, /mob/living/basic/mouse)) diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 4d7a26858a2b..c9140515a061 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -36,10 +36,11 @@ #define DEFAULT_PLANE 0 //Marks out the default plane, even if we don't use it #define WEATHER_PLANE 1 -#define AREA_PLANE 2 -#define MASSIVE_OBJ_PLANE 3 -#define GHOST_PLANE 4 -#define POINT_PLANE 5 +#define PARTICLE_WEATHER_PLANE 2 +#define AREA_PLANE 3 +#define MASSIVE_OBJ_PLANE 4 +#define GHOST_PLANE 5 +#define POINT_PLANE 6 //---------- LIGHTING ------------- /// Normal 1 per turf dynamic lighting objects diff --git a/code/__DEFINES/maps.dm b/code/__DEFINES/maps.dm index f7f1e15b1d44..63f1b4b0f636 100644 --- a/code/__DEFINES/maps.dm +++ b/code/__DEFINES/maps.dm @@ -135,7 +135,7 @@ Always compile, always use that verb, and always make sure that it works for wha ///Z level traits for Deep Space #define ZTRAITS_SPACE list(ZTRAIT_LINKAGE = CROSSLINKED, ZTRAIT_SPACE_RUINS = TRUE) ///Z level traits for -#define ZTRAITS_WILDS list(\ +#define ZTRAITS_ICY_WILDS list(\ ZTRAIT_LINKAGE = GRIDLINKED, \ ZTRAIT_ICE_RUINS = TRUE, \ ZTRAIT_SNOWSTORM = FALSE, \ diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index 0f4a28a1f940..82809492ec03 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -1096,6 +1096,15 @@ GLOBAL_LIST_INIT(layers_to_offset, list( /// Helper macro that determines if the mob is at the threshold to start vomitting due to high toxin levels #define AT_TOXIN_VOMIT_THRESHOLD(mob) (mob.get_tox_loss() > 45 && mob.nutrition > 20) +/// Shared cooldown for manually triggered emote audio +#define MANUAL_GENERAL_EMOTE_AUDIO_COOLDOWN "manual_general_emote_audio_cooldown" +/// Per emote cooldown for manually triggered emote audio +#define MANUAL_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type) "manual_specific_emote_audio_cooldown_[type]" +/// Shared cooldown for forced emote audio +#define FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN "forced_general_emote_audio_cooldown" +/// Per emote cooldown for forced emote audio +#define FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type) "forced_specific_emote_audio_cooldown_[type]" + /// The duration of the flip emote animation #define FLIP_EMOTE_DURATION 0.7 SECONDS ///The duration of a taunt emote, so how long they can deflect projectiles diff --git a/code/__DEFINES/religion.dm b/code/__DEFINES/religion.dm index d3fb3d36e410..694968e0356d 100644 --- a/code/__DEFINES/religion.dm +++ b/code/__DEFINES/religion.dm @@ -68,3 +68,20 @@ #define RITE_ALLOW_MULTIPLE_PERFORMS (1<<1) ///The rite can only be fully performed once, so we'll completely remove it from the rite list afterwards. #define RITE_ONE_TIME_USE (1<<2) + +///Default prayer type for the pray verb +#define DEFAULT_PRAYER "PRAYER" +///Cult prayer type +#define CULT_PRAYER "CULTIST PRAYER" +//Heretic prayer type +#define HERETIC_PRAYER "HERETICAL PRAYER" +///Chaplain prayer type +#define CHAPLAIN_PRAYER "CHAPLAIN PRAYER" +///Spiritual prayer type +#define SPIRITUAL_PRAYER "SPIRITUAL PRAYER" +///Evil prayer type +#define EVIL_PRAYER "EVIL PRAYER" +///Standard prayer type that santa sees +#define SANTA_PRAYER "PRAYER" +///what type of prayer santa sees when someone's bad +#define SANTA_NAUGHTY_PRAYER "PRAYER (NAUGHTY)" diff --git a/code/__DEFINES/robots.dm b/code/__DEFINES/robots.dm index 4a6b0fa19b73..edd92fcc1b82 100644 --- a/code/__DEFINES/robots.dm +++ b/code/__DEFINES/robots.dm @@ -403,3 +403,10 @@ DEFINE_BITFIELD(janitor_mode_flags, list( /// Default offsets for riding a cyborg #define DEFAULT_ROBOT_RIDING_OFFSETS list(TEXT_NORTH = list(0, 4), TEXT_SOUTH = list(0, 4), TEXT_EAST = list(-6, 3), TEXT_WEST = list(6, 3)) + + +//mulebots +#define MULEBOT_MOOD_ANNOYED "ANNOYED" +#define MULEBOT_MOOD_CHIME "CHIME" +#define MULEBOT_MOOD_DELIGHT "DELIGHT" +#define MULEBOT_MOOD_SIGH "SIGH" diff --git a/code/__DEFINES/say.dm b/code/__DEFINES/say.dm index 8f357a9b5db9..5e0d76bef0da 100644 --- a/code/__DEFINES/say.dm +++ b/code/__DEFINES/say.dm @@ -11,7 +11,8 @@ #define LANGUAGE_MUTUAL_BONUS "language mutual bonus" #define SAY_MOD_VERB "say_mod_verb" -#define HEARD_BUT_DIDNT_UNDERSTAND "heard_but_didnt_understand" +#define HEAR_HEARD (1<<0) +#define HEAR_UNDERSTOOD (1<<1) //Message modes. Each one defines a radio channel, more or less. //if you use ! as a mode key for some ungodly reason, change the first character for ion_num() so get_message_mode() doesn't freak out with state law prompts - shiz. @@ -76,7 +77,8 @@ /// Message is being relayed through another object #define MODE_RELAY "relayed" /// Message has a TTS identifier attached to it -#define MODE_TTS_IDENTIFIER "tts_identifier"/// Override the mob's name +#define MODE_TTS_IDENTIFIER "tts_identifier" +/// Override the mob's name #define MODE_SPEAKER_NAME_OVERRIDE "speaker_name_override" //Spans. Robot speech, italics, etc. Applied in compose_message(). diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index ff73ad03c525..1f859b9b5342 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -381,6 +381,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_SUCCUMB_OVERRIDE "succumb_override" /// Can hear observers #define TRAIT_SIXTHSENSE "sixth_sense" +/// For mobs / minds with the listening_prayers trait, this stops them from hearing prayers +#define TRAIT_DONT_HEAR_PRAYERS "dont_hear_prayers" #define TRAIT_FEARLESS "fearless" /// Ignores darkness for hearing #define TRAIT_HEAR_THROUGH_DARKNESS "hear_through_darkness" diff --git a/code/__HELPERS/names.dm b/code/__HELPERS/names.dm index 91a6ccfe62ae..cb9470bf6bd7 100644 --- a/code/__HELPERS/names.dm +++ b/code/__HELPERS/names.dm @@ -68,9 +68,6 @@ /mob/living/basic/bot/generate_random_mob_name(unique) return generate_random_name(gender, unique, list(/datum/language/machine = 1)) -/mob/living/simple_animal/bot/generate_random_mob_name(unique) - return generate_random_name(gender, unique, list(/datum/language/machine = 1)) - GLOBAL_VAR(command_name) /proc/command_name() if (GLOB.command_name) @@ -186,15 +183,6 @@ GLOBAL_VAR(command_name) return name - -//Traitors and traitor silicons will get these. Revs will not. -GLOBAL_VAR(syndicate_code_phrase) //Code phrase for traitors. -GLOBAL_VAR(syndicate_code_response) //Code response for traitors. - -//Cached regex search - for checking if codewords are used. -GLOBAL_DATUM(syndicate_code_phrase_regex, /regex) -GLOBAL_DATUM(syndicate_code_response_regex, /regex) - /* Should be expanded. How this works: @@ -223,14 +211,16 @@ GLOBAL_DATUM(syndicate_code_response_regex, /regex) 25; 5 ) - var/list/safety = list(1,2,3)//Tells the proc which options to remove later on. - var/nouns = strings(ION_FILE, "ionabstract") - var/objects = strings(ION_FILE, "ionobjects") - var/adjectives = strings(ION_FILE, "ionadjectives") - var/threats = strings(ION_FILE, "ionthreats") - var/foods = strings(ION_FILE, "ionfood") - var/drinks = strings(ION_FILE, "iondrinks") - var/locations = strings(LOCATIONS_FILE, "locations") + var/list/safety = list(1, 2, 3)//Tells the proc which options to remove later on. + var/list/nouns = strings(ION_FILE, "ionabstract") + var/list/objects = strings(ION_FILE, "ionobjects") + var/list/adjectives = strings(ION_FILE, "ionadjectives") + var/list/threats = strings(ION_FILE, "ionthreats") + var/list/foods = strings(ION_FILE, "ionfood") + var/list/drinks = strings(ION_FILE, "iondrinks") + var/list/locations = list() + for(var/area/area_type as anything in list(/area/space) | GLOB.the_station_areas) + locations |= format_text(area_type::name) //MASSMETA EDIT BEGIN (ru_traitor_words) // var/list/names = list() diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm index 4781049dd821..87301d95429a 100644 --- a/code/__HELPERS/spatial_info.dm +++ b/code/__HELPERS/spatial_info.dm @@ -220,8 +220,15 @@ // Returns a list of mobs who can hear any of the radios given in @radios, indexed by the radio. More expensive, but needed for radio TTS to sound good. for(var/obj/item/radio/radio as anything in radios) var/list/possible_hearers = get_hearers_in_LOS(radio.canhear_range, radio) - if(LAZYLEN(possible_hearers)) + /*if(LAZYLEN(possible_hearers)) + .[radio] = filter_tts_listeners(possible_hearers, frequency) + */ + var/list/weakref_hearers = list() + for(var/ref in possible_hearers) + weakref_hearers += WEAKREF(ref) + if(LAZYLEN(possible_hearers)) + .[WEAKREF(radio)] = filter_tts_listeners(weakref_hearers, frequency) /// A filter to be applied to get_hearers_in_x, that removes any non-mob hearers, converting them to their relevant mob if one exists (such as dullahan heads). diff --git a/code/__HELPERS/tts.dm b/code/__HELPERS/tts.dm index 5e1914bda084..6a2011c47e24 100644 --- a/code/__HELPERS/tts.dm +++ b/code/__HELPERS/tts.dm @@ -5,7 +5,9 @@ /proc/tts_gibberish_speech_filter(text) // Only allow alphanumeric characters and whitespace + // MASSMETA EDIT var/static/regex/bad_chars_regex = regex("\[^а-яА-ЯёЁa-zA-Z0-9 ,?.!'&-]", "g") + // MASSMETA EDIT return bad_chars_regex.Replace(text, "#") /proc/tts_filter_encode(text, speaker, pitch, blips = FALSE) diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm index 186385debfe7..9d8ea11ce869 100644 --- a/code/_globalvars/phobias.dm +++ b/code/_globalvars/phobias.dm @@ -98,7 +98,6 @@ GLOBAL_LIST_INIT(phobia_mobs, list( /mob/living/basic/drone, /mob/living/silicon/ai, /mob/living/silicon/robot, - /mob/living/simple_animal/bot, )), "security" = typecacheof(list(/mob/living/basic/bot/secbot)), "spiders" = typecacheof(list( @@ -123,7 +122,7 @@ GLOBAL_LIST_INIT(phobia_mobs, list( /mob/living/basic/skeleton, /mob/living/basic/wizard, /mob/living/basic/zombie, - /mob/living/simple_animal/bot/mulebot/paranormal, + /mob/living/basic/bot/mulebot/paranormal, /mob/living/basic/voidwalker, )), )) diff --git a/code/_globalvars/religion.dm b/code/_globalvars/religion.dm index 063ff0e86428..c2ea1a69d350 100644 --- a/code/_globalvars/religion.dm +++ b/code/_globalvars/religion.dm @@ -15,6 +15,39 @@ GLOBAL_LIST_EMPTY(chaplain_altars) GLOBAL_VAR(holy_weapon_type) GLOBAL_VAR(holy_armor_type) +GLOBAL_LIST_INIT(prayer_type_to_font_color, list( + DEFAULT_PRAYER = "purple", + CULT_PRAYER = "black", + HERETIC_PRAYER = "green", + CHAPLAIN_PRAYER = "yellow", + SPIRITUAL_PRAYER = "blue", + EVIL_PRAYER = "red", + SANTA_PRAYER = "purple", + SANTA_NAUGHTY_PRAYER = "red", +)) + +GLOBAL_LIST_INIT(prayer_type_to_icon_state, list( + DEFAULT_PRAYER = "bible", + CULT_PRAYER = "tome", + HERETIC_PRAYER = "necronomicon", + CHAPLAIN_PRAYER = "kingyellow", + SPIRITUAL_PRAYER = "holylight", + EVIL_PRAYER = "burning", + SANTA_PRAYER = "bible", //here just in case, we use present boxes for the icon + SANTA_NAUGHTY_PRAYER = "burning", +)) + +GLOBAL_LIST_INIT(prayer_type_to_message_box, list( + DEFAULT_PRAYER = "", + CULT_PRAYER = "red_box", + HERETIC_PRAYER = "green_box", + CHAPLAIN_PRAYER = "blue_box", + SPIRITUAL_PRAYER = "", + EVIL_PRAYER = "red_box", + SANTA_PRAYER = "blue_box", + SANTA_NAUGHTY_PRAYER = "red_box", +)) + /// Sets a new religious sect used by all chaplains int he round /proc/set_new_religious_sect(path, reset_existing = FALSE) if(!ispath(path, /datum/religion_sect)) diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 498cf5b986e7..8ae885f9e7e1 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -273,6 +273,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_DISK_VERIFIER" = TRAIT_DISK_VERIFIER, "TRAIT_DISPLAY_JOB_IN_BINARY" = TRAIT_DISPLAY_JOB_IN_BINARY, "TRAIT_DISSECTED" = TRAIT_DISSECTED, + "TRAIT_DONT_HEAR_PRAYERS" = TRAIT_DONT_HEAR_PRAYERS, "TRAIT_DONT_WRITE_MEMORY" = TRAIT_DONT_WRITE_MEMORY, "TRAIT_DOUBLE_TAP" = TRAIT_DOUBLE_TAP, "TRAIT_DREAMING" = TRAIT_DREAMING, diff --git a/code/_onclick/hud/ooze.dm b/code/_onclick/hud/ooze.dm index e796df33b4aa..5d0fa005fe2e 100644 --- a/code/_onclick/hud/ooze.dm +++ b/code/_onclick/hud/ooze.dm @@ -1,5 +1,5 @@ ///Hud type with targeting dol and a nutrition bar -/datum/hud/ooze/initialize_screen_objects() +/datum/hud/living/ooze/initialize_screen_objects() . = ..() add_screen_object(/atom/movable/screen/zone_sel, HUD_MOB_ZONE_SELECTOR, HUD_GROUP_STATIC, ui_style) add_screen_object(/atom/movable/screen/ooze_nutrition_display, HUD_OOZE_NUTRITION_DISPLAY) diff --git a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm index 094371cc9e68..b5dc090c9aaa 100644 --- a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm +++ b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm @@ -316,17 +316,40 @@ plane = AREA_PLANE /atom/movable/screen/plane_master/weather - name = "Weather" - documentation = "Holds the main tiling 32x32 sprites of weather. We mask against walls that are on the edge of weather effects." + name = "Non-Particle Weather" + documentation = "Holds the main tiling 32x32 sprites of weather. We mask against walls that are on the edge of weather effects. Used when the player has particle weather disabled." plane = WEATHER_PLANE start_hidden = TRUE critical = PLANE_CRITICAL_DISPLAY + /// Is this a particle variant? + var/particle_weather = FALSE /atom/movable/screen/plane_master/weather/set_home(datum/plane_master_group/home) . = ..() if(!.) return - home.AddComponent(/datum/component/hide_weather_planes, src) + home.AddComponent(/datum/component/hide_weather_planes, src, particle_weather) + +/atom/movable/screen/plane_master/weather/proc/update_state(mob/mymob) + if(!istype(mymob)) + return + + // If the client wants particle weather, only show the PARTICLE_WEATHER_PLANE, otherwise only show the normal WEATHER_PLANE + if (mymob.canon_client?.prefs?.read_preference(/datum/preference/toggle/particle_weather) != particle_weather) + hide_from(mymob) + else + show_to(mymob) + +/atom/movable/screen/plane_master/weather/show_to(mob/mymob) + // Only show ourselves if the player wants it + if (mymob.canon_client?.prefs?.read_preference(/datum/preference/toggle/particle_weather) == particle_weather) + return ..() + +/atom/movable/screen/plane_master/weather/particle + name = "Particle Weather" + documentation = "Holds the main tiling 32x32 sprites of weather. Used when the player has particle weather enabled." + plane = PARTICLE_WEATHER_PLANE + particle_weather = TRUE /atom/movable/screen/plane_master/weather_mask name = "Weather Mask" diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm index 11979dde2ace..7dd681d378cd 100644 --- a/code/_onclick/hud/rendering/render_plate.dm +++ b/code/_onclick/hud/rendering/render_plate.dm @@ -84,6 +84,27 @@ . = ..() // We can actually do this just fine as we do not render anything onto ourselves but our particles add_filter("weather_mask", 1, alpha_mask_filter(render_source = OFFSET_RENDER_TARGET(WEATHER_MASK_RENDER_TARGET, offset))) + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/set_home(datum/plane_master_group/home) + . = ..() + if(!.) + return + home.AddComponent(/datum/component/hide_weather_planes, src, TRUE) + RegisterSignal(home, COMSIG_GROUP_HUD_CHANGED, PROC_REF(hud_changed)) + update_state(home.our_hud?.mymob) + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/proc/hud_changed(datum/source, datum/hud/old_hud, datum/hud/new_hud) + SIGNAL_HANDLER + update_state(new_hud?.mymob) + +/// Updates ourselves based on our mob's preferences state +/atom/movable/screen/plane_master/rendering_plate/particle_weather/proc/update_state(mob/mymob) + SSweather.particle_planemasters -= src + vis_contents.Cut() + + if(!istype(mymob) || !mymob.canon_client?.prefs?.read_preference(/datum/preference/toggle/particle_weather)) + return + SSweather.particle_planemasters += src // And add all ongoing weather to ourselves for (var/holder_offset, holder_list in SSweather.particle_holders) @@ -95,12 +116,6 @@ SSweather.particle_planemasters -= src return ..() -/atom/movable/screen/plane_master/rendering_plate/particle_weather/set_home(datum/plane_master_group/home) - . = ..() - if(!.) - return - home.AddComponent(/datum/component/hide_weather_planes, src, TRUE) - /atom/movable/screen/plane_master/rendering_plate/particle_weather/emissive name = "Emissive Particle Weather" documentation = "Secondary particle weather plane for emissive parts of weather, which is additionally rendered onto the emissive plane after being masked." diff --git a/code/controllers/subsystem/ambience.dm b/code/controllers/subsystem/ambience.dm index 79f5e7dbd1a0..9cfc81a3f03d 100644 --- a/code/controllers/subsystem/ambience.dm +++ b/code/controllers/subsystem/ambience.dm @@ -20,13 +20,17 @@ SUBSYSTEM_DEF(ambience) var/client/client_iterator = cached_clients[cached_clients.len] cached_clients.len-- - //Check to see if the client exists and isn't held by a new player - var/mob/client_mob = client_iterator?.mob - if(isnull(client_iterator) || !client_mob || isnewplayer(client_mob)) + //Check to see if the client exists + if(isnull(client_iterator)) ambience_listening_clients -= client_iterator client_old_areas -= client_iterator continue + // skip them this tick if they're on the lobby screen or somehow dont have a mob?? + var/mob/client_mob = client_iterator?.mob + if(!client_mob || isnewplayer(client_mob)) + continue + if(HAS_TRAIT(client_mob, TRAIT_DEAF)) //WHAT? I CAN'T HEAR YOU continue diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm index 2143b6cbb001..3df350732471 100644 --- a/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm +++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm @@ -442,6 +442,7 @@ new_head.give_flash = TRUE new_head.give_hud = TRUE new_head.remove_clumsy = TRUE + new_head.roundstart = TRUE candidate.add_antag_datum(new_head, GLOB.revolution_handler.revs) GLOB.revolution_handler.start_revolution() diff --git a/code/controllers/subsystem/map_vote.dm b/code/controllers/subsystem/map_vote.dm index 4229ee82737b..6f0f3a501da2 100644 --- a/code/controllers/subsystem/map_vote.dm +++ b/code/controllers/subsystem/map_vote.dm @@ -22,6 +22,9 @@ SUBSYSTEM_DEF(map_vote) /// Stores the last amount of potential players to compare next time we're called var/player_cache = -1 + /// Cached list of votable maps + var/list/votable_map_cache + /// Stores a formatted html string of the tally counts var/tally_printout = span_red("Loading...") @@ -113,8 +116,9 @@ SUBSYSTEM_DEF(map_vote) else filter_threshold = length(GLOB.clients) - if(filter_threshold == player_cache) - return null + // Cached because it's called off ui_data, I really don't think this is worth it but whatever + if(filter_threshold == player_cache && !isnull(votable_map_cache)) + return votable_map_cache.Copy() player_cache = filter_threshold var/list/valid_maps = list() @@ -130,6 +134,7 @@ SUBSYSTEM_DEF(map_vote) continue valid_maps += possible_config.map_name + votable_map_cache = valid_maps.Copy() return valid_maps /datum/controller/subsystem/map_vote/proc/filter_cache_to_valid_maps() diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index 7b1b55341c51..274325956a4c 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -144,7 +144,7 @@ SUBSYSTEM_DEF(mapping) if(current_map.wilderness_levels) var/list/FailedZs = list() - LoadGroup(FailedZs, "Wilderness Area", current_map.wilderness_directory, current_map.maps_to_spawn, default_traits = ZTRAITS_WILDS, height_autosetup = FALSE) + LoadGroup(FailedZs, "Wilderness Area", current_map.wilderness_directory, current_map.wilderness_maps_to_spawn, default_traits = current_map.wilderness_z_traits, height_autosetup = FALSE) if(LAZYLEN(FailedZs)) CRASH("Ice wilds failed to load!") diff --git a/code/controllers/subsystem/minor_mapping.dm b/code/controllers/subsystem/minor_mapping.dm index 8e57d6782832..7e1b54eab7f9 100644 --- a/code/controllers/subsystem/minor_mapping.dm +++ b/code/controllers/subsystem/minor_mapping.dm @@ -11,7 +11,8 @@ SUBSYSTEM_DEF(minor_mapping) var/list/vermin_chances = list( /mob/living/basic/mouse = 72, /mob/living/basic/snail = 16, - /mob/living/basic/stoat = 10, + /mob/living/basic/stoat = 7, + /mob/living/basic/stoat/kit = 3, /mob/living/basic/regal_rat/controlled = 2, ) diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index e83f68d22dbe..ea1e7f90cda0 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -461,6 +461,94 @@ if(length(movement_path)) movement_path.Cut(1,2) else + return handle_move_attempt_failure() + + +/datum/move_loop/has_target/jps/proc/handle_move_attempt_failure() + EVLOG_TEXT(moving, EVLOG_CATEGORY_MOVELOOPS, "Path recalculating due to obstruction") + INVOKE_ASYNC(src, PROC_REF(recalculate_path)) + return MOVELOOP_FAILURE + +/datum/move_loop/has_target/jps/frustrations + ///maximum amount of frustrations before we recalculate path + var/maximum_frustrations + ///what is our current frustration? + var/current_frustrations = 0 + ///how long before we're able to increment frustration? + var/frustration_delay + ///have we drawn our initial path? + var/initial_path_drawn = FALSE + ///cooldown between frustration increments + COOLDOWN_DECLARE(frustration_cooldown) + + +/datum/move_manager/proc/frustrations_move(moving, + chasing, + delay, + timeout, + repath_delay, + max_path_length, + minimum_distance, + list/access, + simulated_only, + turf/avoid, + skip_first, + subsystem, + diagonal_handling, + priority, + flags, + datum/extra_info, + initial_path) + return add_to_loop(moving, + subsystem, + /datum/move_loop/has_target/jps/frustrations, + priority, + flags, + extra_info, + delay, + timeout, + chasing, + repath_delay, + max_path_length, + minimum_distance, + access, + simulated_only, + avoid, + skip_first, + diagonal_handling, + initial_path) + +/datum/move_loop/has_target/jps/frustrations/setup(delay, timeout, atom/chasing, maximum_frustrations = 10, frustration_delay = 2 SECONDS) + . = ..() + if(!.) + return + src.maximum_frustrations = maximum_frustrations + src.frustration_delay = frustration_delay + +/datum/move_loop/has_target/jps/frustrations/recalculate_path() + if(initial_path_drawn && current_frustrations < maximum_frustrations) + return + return ..() + +/datum/move_loop/has_target/jps/frustrations/loop_stopped() + . = ..() + +/datum/move_loop/has_target/jps/frustrations/on_finish_pathing(list/path) + . = ..() + if(movement_path) + initial_path_drawn = TRUE + +/datum/move_loop/has_target/jps/frustrations/handle_move_attempt_failure() + if(!initial_path_drawn) + INVOKE_ASYNC(src, PROC_REF(recalculate_path)) + return MOVELOOP_FAILURE + if(!COOLDOWN_FINISHED(src, frustration_cooldown)) + return NONE + COOLDOWN_START(src, frustration_cooldown, frustration_delay) + current_frustrations++ + SEND_SIGNAL(src, COMSIG_MOVELOOP_JPS_FRUSTRATION_INCREMENTED, current_frustrations) + if(current_frustrations >= maximum_frustrations) + current_frustrations = 0 EVLOG_TEXT(moving, EVLOG_CATEGORY_MOVELOOPS, "Path recalculating due to obstruction") INVOKE_ASYNC(src, PROC_REF(recalculate_path)) return MOVELOOP_FAILURE diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index 7c173ae293cf..e11c60cb3341 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -37,7 +37,7 @@ SUBSYSTEM_DEF(statpanels) global_data += list( "Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]", - "Server Time/NST: [server_timestamp(format = "YYYY-MM-DD hh:mm:ss")]", + "Server Time/NST: [server_timestamp(format = "YYYY-MM-DD hh:mm:ss", ic_time = TRUE)]", "Shift Time/PT: [(SSticker.round_start_time == 0) ? "Pre-Game" : round_timestamp()]", "Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)", ) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 06a13b68b829..4dbe10f0def8 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -111,22 +111,6 @@ SUBSYSTEM_DEF(ticker) else set_lobby_music("[global.config.directory]/title_music/sounds/[pick(music)]") - if(!GLOB.syndicate_code_phrase) - GLOB.syndicate_code_phrase = generate_code_phrase(return_list=TRUE) - - var/codewords = jointext(GLOB.syndicate_code_phrase, "|") - var/regex/codeword_match = new("([codewords])", "ig") - - GLOB.syndicate_code_phrase_regex = codeword_match - - if(!GLOB.syndicate_code_response) - GLOB.syndicate_code_response = generate_code_phrase(return_list=TRUE) - - var/codewords = jointext(GLOB.syndicate_code_response, "|") - var/regex/codeword_match = new("([codewords])", "ig") - - GLOB.syndicate_code_response_regex = codeword_match - start_at = world.time + (CONFIG_GET(number/lobby_countdown) * (1 SECONDS)) return SS_INIT_SUCCESS @@ -141,9 +125,9 @@ SUBSYSTEM_DEF(ticker) to_chat(world, span_notice("Welcome to [station_name()]!")) for(var/channel_tag in CONFIG_GET(str_list/channel_announce_new_game)) send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.current_map.map_name]!"), channel_tag) + current_state = GAME_STATE_PREGAME SEND_SIGNAL(src, COMSIG_TICKER_ENTER_PREGAME) - fire() if(GAME_STATE_PREGAME) //lobby stats for statpanels diff --git a/code/controllers/subsystem/traitor.dm b/code/controllers/subsystem/traitor.dm index edb9fbba5cda..e7ecf385aa74 100644 --- a/code/controllers/subsystem/traitor.dm +++ b/code/controllers/subsystem/traitor.dm @@ -32,11 +32,20 @@ SUBSYSTEM_DEF(traitor) var/current_global_progression = 0 /// The amount of deviance from the current global progression before you start getting 2x the current scaling or no scaling at all + /// The current scaling per minute of progression. + var/current_progression_scaling = 1 MINUTES + /// List of code words for traitors + var/syndicate_code_phrase + /// List of code responses for traitors + var/syndicate_code_response + /// Regex of code words for traitors + var/regex/syndicate_code_phrase_regex + /// Regex of code responses for traitors + var/regex/syndicate_code_response_regex + //MASSMETA ADDDITION START (re_traitorsecondary) var/list/datum/uplink_handler/uplink_handlers = list() - /// The current scaling per minute of progression. Has a maximum value of 1 MINUTES. - var/current_progression_scaling = 1 MINUTES /// Used to handle the probability of getting an objective. var/datum/traitor_category_handler/category_handler /// The current debug handler for objectives. Used for debugging objectives @@ -67,6 +76,11 @@ SUBSYSTEM_DEF(traitor) log_world("[configuration_path] has an invalid type ([typepath]) that doesn't exist in the codebase! Please correct or remove [typepath]") configuration_data[actual_typepath] = data[typepath] //MASSMETA ADDITION END (re_traitor_secondary) + + syndicate_code_phrase = generate_code_phrase(return_list = TRUE) + syndicate_code_phrase_regex = new("([jointext(syndicate_code_phrase, "|")])", "ig") + syndicate_code_response = generate_code_phrase(return_list = TRUE) + syndicate_code_response_regex = new("([jointext(syndicate_code_response, "|")])", "ig") return SS_INIT_SUCCESS /datum/controller/subsystem/traitor/fire(resumed) diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm index e0460c1aa2af..9c84945cac01 100644 --- a/code/controllers/subsystem/tts.dm +++ b/code/controllers/subsystem/tts.dm @@ -228,22 +228,30 @@ SUBSYSTEM_DEF(tts) return SS_INIT_FAILURE return SS_INIT_SUCCESS -/datum/controller/subsystem/tts/proc/play_tts(target, list/listeners, sound/audio, sound/audio_blips, datum/language/language, range = 7, volume_offset = 0, ignore_observers = FALSE, source_speaker = null, audio_length = 10 SECONDS, audio_length_blips = 10 SECONDS, volume_preference = /datum/preference/numeric/volume/sound_tts_volume, volume_signal = COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED) - var/turf/turf_source = get_turf(target) - if(!turf_source && target) // if there's a target, we better have a turf +/datum/controller/subsystem/tts/proc/play_tts(datum/weakref/target, list/listeners, sound/audio, sound/audio_blips, datum/language/language, range = 7, volume_offset = 0, ignore_observers = FALSE, source_speaker = null, audio_length = 10 SECONDS, audio_length_blips = 10 SECONDS, volume_preference = /datum/preference/numeric/volume/sound_tts_volume, volume_signal = COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED) + var/atom/actual_target = target?.resolve() + var/turf/turf_source + if(actual_target) + turf_source = get_turf(actual_target) + if(!turf_source && actual_target) // if there's a target, we better have a turf return var/channel = SSsounds.random_available_channel() var/list/final_listeners = listeners - if(!ignore_observers && target) + if(!ignore_observers && actual_target) final_listeners += SSmobs.dead_players_by_zlevel[turf_source.z] //observers always hear through walls var/list/blips_hearers = list() var/list/voice_hearers = list() for(var/hearer in final_listeners) if(isnull(hearer)) continue - var/atom/movable/hearer_atom = hearer - if(QDELING(hearer_atom)) + var/atom/movable/hearer_atom + if(isweakref(hearer)) + var/datum/weakref/weakref = hearer + hearer_atom = weakref?.resolve() + else + hearer_atom = hearer + if(!hearer_atom || QDELING(hearer_atom)) stack_trace("TTS tried to play a sound to a deleted mob.") continue if(!ismob(hearer_atom)) @@ -258,7 +266,7 @@ SUBSYSTEM_DEF(tts) if(listening_mob == source_speaker && !hear_self_pref) continue // don't hear your own radio tts if you got it turned off - var/sound_volume = ((hearer == target)? 60 : 85) + volume_offset + var/sound_volume = ((listening_mob == actual_target)? 60 : 85) + volume_offset sound_volume = sound_volume*volume_modifier var/datum/language_holder/holder = listening_mob.get_language_holder() var/sound/audio_to_use = (tts_pref == TTS_SOUND_BLIPS) ? audio_blips : audio @@ -267,7 +275,7 @@ SUBSYSTEM_DEF(tts) continue else audio_to_use = audio_blips - if(target && get_dist(hearer, turf_source) <= range) + if(actual_target && get_dist(listening_mob, turf_source) <= range) if(tts_pref == TTS_SOUND_BLIPS || !holder.has_language(language)) blips_hearers += listening_mob else @@ -284,7 +292,7 @@ SUBSYSTEM_DEF(tts) distance_multiplier = 1, use_reverb = TRUE ) - else if(!target) + else if(!actual_target) listening_mob.playsound_local( null, //play it locally vol = sound_volume, @@ -297,30 +305,30 @@ SUBSYSTEM_DEF(tts) distance_multiplier = 1, use_reverb = TRUE ) - if(target) + if(actual_target) new /datum/threed_sound( - target, - audio, - voice_hearers, - FALSE, - 85 + volume_offset, - SOUND_RANGE, - audio_length, - channel, - volume_preference, - volume_signal + new_parent = actual_target, + new_sound = audio, + current_listeners = voice_hearers, + can_add_new_listeners = FALSE, + volume = 85 + volume_offset, + sound_range = SOUND_RANGE, + sound_length = audio_length, + channel = channel, + preference_volume = volume_preference, + preference_signal = volume_signal ) new /datum/threed_sound( - target, - audio_blips, - blips_hearers, - FALSE, - 85 + volume_offset, - SOUND_RANGE, - audio_length_blips, - channel, - volume_preference, - volume_signal + new_parent = actual_target, + new_sound = audio_blips, + current_listeners = blips_hearers, + can_add_new_listeners = FALSE, + volume = 85 + volume_offset, + sound_range = SOUND_RANGE, + sound_length = audio_length_blips, + channel = channel, + preference_volume = volume_preference, + preference_signal = volume_signal ) @@ -342,7 +350,7 @@ SUBSYSTEM_DEF(tts) var/datum/http_request/request_blips = data.request_blips var/datum/http_request/request_radio = data.request_radio var/datum/http_request/request_blips_radio = data.request_blips_radio - var/datum/http_request/request_radio_gibberish = data.request_radio_gibberish + var/datum/http_request/request_radio_gibberish = data.request_radio_gibberish // ????? UNTIL(request.is_complete() && request_blips.is_complete() && request_radio.is_complete() && request_blips_radio.is_complete() && (!request_radio_gibberish || request_radio_gibberish.is_complete())) // MASSMETA EDIT END (ntts && /tg/tts) @@ -398,11 +406,9 @@ SUBSYSTEM_DEF(tts) if(current_request.requests_errored()) if(queued_radio_messages[identifier]) queued_radio_messages.Remove(identifier) - // MASSMETA EDIT START (ntts && /tg/tts) - queued_radio_messages_compression.Remove(identifier) - // MASSMETA EDIT END (ntts && /tg/tts) + if(queued_radio_messages_compression[identifier]) + queued_radio_messages_compression.Remove(identifier) current_request.timed_out = TRUE - // MASSMETA EDIT START (ntts && /tg/tts) log_tts("TTS HTTP request errored | Normal: [normal_response.status_code] [normal_response.error] | Blips: [blips_response.status_code] [blips_response.error] | Radio: [radio_response.status_code] [radio_response.error] | Radio Blips: [radio_blips_response.status_code] [radio_blips_response.error] | Radio Gibberish: [radio_gibberish_response.status_code] [radio_gibberish_response.error]", list( "normal" = normal_response, "blips" = blips_response, @@ -418,13 +424,20 @@ SUBSYSTEM_DEF(tts) current_request.audio_length = 0 // MASSMETA EDIT START (ntts && /tg/tts) + if(!current_request.announcement && length(blips_response.headers) && blips_response.headers.Find("audio-length")) current_request.audio_length_blips = text2num(blips_response.headers["audio-length"]) * 10 + + // MASSMETA EDIT END (ntts && /tg/tts) + if(!current_request.audio_length_blips) current_request.audio_length_blips = 0 + // MASSMETA EDIT START (ntts && /tg/tts) if(!current_request.announcement) if(length(radio_response.headers) && radio_response.headers.Find("audio-length")) current_request.audio_length_radio = text2num(radio_response.headers["audio-length"]) * 10 + // MASSMETA EDIT END (ntts && /tg/tts) + if(!current_request.audio_length_radio) current_request.audio_length_radio = 0 if(length(radio_blips_response.headers) && radio_blips_response.headers.Find("audio-length")) @@ -435,22 +448,20 @@ SUBSYSTEM_DEF(tts) current_request.audio_length_radio_gibberish = text2num(radio_gibberish_response.headers["audio-length"]) * 10 if(!current_request.audio_length_radio_gibberish) current_request.audio_length_radio_gibberish = 0 - // MASSMETA EDIT END (ntts && /tg/tts) + current_request.audio_file = "tmp/tts/[identifier].ogg" - // MASSMETA EDIT START (ntts && /tg/tts) + + if(!current_request.announcement) current_request.audio_file_blips = "tmp/tts/[identifier]_blips.ogg" // We aren't as concerned about the audio length for blips as we are with actual speech current_request.audio_file_radio = "tmp/tts/[identifier]_radio.ogg" current_request.audio_file_blips_radio = "tmp/tts/[identifier]_blips_radio.ogg" current_request.audio_file_radio_gibberish = current_request.request_radio_gibberish ? "tmp/tts/[identifier]_radio_gibberish.ogg" : current_request.audio_file_radio - // MASSMETA EDIT END (ntts && /tg/tts) // Don't need the request anymore so we can deallocate it current_request.request = null current_request.request_blips = null - // MASSMETA EDIT START (ntts && /tg/tts) current_request.request_radio = null current_request.request_blips_radio = null - // MASSMETA EDIT END (ntts && /tg/tts) current_request.request_radio_gibberish = null if(MC_TICK_CHECK) return @@ -485,6 +496,10 @@ SUBSYSTEM_DEF(tts) // If current_target.timed_out is set to TRUE, it means the request failed in some way // and there is no TTS audio file to play. if(timeout < world.time || current_target.timed_out) + if(queued_radio_messages[current_target.identifier]) + queued_radio_messages.Remove(current_target.identifier) + if(queued_radio_messages_compression[current_target.identifier]) + queued_radio_messages_compression.Remove(current_target.identifier) SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data) continue @@ -545,17 +560,19 @@ SUBSYSTEM_DEF(tts) var/list/all_radios = queued_radio_messages[identifier] for(var/radio in all_radios) var/list/hearers = all_radios[radio] - if(!istext(radio)) - var/obj/radio_obj = radio - if(QDELETED(radio_obj)) + if(!istext(radio) && isweakref(radio)) + var/datum/weakref/weakref = radio + var/obj/radio_obj = weakref?.resolve() + if(radio_obj && QDELETED(radio_obj)) queued_radio_messages[identifier].Remove(radio) + queued_radio_messages_compression[identifier].Remove(radio) continue var/datum/tts_request/tts_request = completed_tts_messages[identifier]["ref"] var/sound/audio_file var/sound/audio_file_blips if(queued_radio_messages_compression[identifier] > 30) - audio_file = new(tts_request.audio_file_radio_gibberish || tts_request.audio_file_radio) + audio_file = new(tts_request.audio_file_radio_gibberish) else audio_file = new(tts_request.audio_file_radio) audio_file_blips = new(tts_request.audio_file_blips_radio) @@ -591,7 +608,9 @@ SUBSYSTEM_DEF(tts) var/shell_scrubbed_input = tts_speech_filter(message) if(!(speaker in available_speakers)) return - + var/list/listener_weakrefs = list() + for(var/listener in listeners) + listener_weakrefs += WEAKREF(listener) var/list/headers = list() headers["Content-Type"] = "application/json" headers["Authorization"] = CONFIG_GET(string/tts_http_token) @@ -601,7 +620,7 @@ SUBSYSTEM_DEF(tts) var/datum/http_request/request_radio = new() var/datum/http_request/request_blips_radio = new() // MASSMETA EDIT END (ntts && /tg/tts) - var/datum/http_request/request_radio_gibberish + var/datum/http_request/request_radio_gibberish = new() var/file_name = "tmp/tts/[identifier].ogg" var/file_name_blips = "tmp/tts/[identifier]_blips.ogg" var/file_name_radio = "tmp/tts/[identifier]_radio.ogg" @@ -614,10 +633,10 @@ SUBSYSTEM_DEF(tts) request_blips_radio.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-blips-radio?voice=[speaker]&identifier=[identifier]&filter=[tts_filter_encode(filter, speaker, pitch, blips = TRUE)]&pitch=[pitch]&special_filters=[url_encode(special_filters)]&blip_base=[blip_base]&blip_number=[blip_number]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_blips_radio, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds)) var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, request_blips, request_radio, request_blips_radio, request_radio_gibberish, shell_scrubbed_input, target, local, language, message_range, volume_offset, listeners, pitch, force_blips) // MASSMETA EDIT END (ntts && /tg/tts) - var/list/player_queued_tts_messages = queued_tts_messages[target] + var/list/player_queued_tts_messages = queued_tts_messages[WEAKREF(target)] if(!player_queued_tts_messages) player_queued_tts_messages = list() - queued_tts_messages[target] = player_queued_tts_messages + queued_tts_messages[WEAKREF(target)] = player_queued_tts_messages player_queued_tts_messages += current_request if(length(in_process_http_messages) < max_concurrent_requests) current_request.start_requests() @@ -702,9 +721,8 @@ SUBSYSTEM_DEF(tts) /// Should we force play blips? Used for the blips preview. var/force_blips = FALSE - // MASSMETA EDIT START (ntts && /tg/tts) + /datum/tts_request/New(identifier, datum/http_request/request, datum/http_request/request_blips, datum/http_request/request_radio, datum/http_request/request_blips_radio, datum/http_request/request_radio_gibberish, message, target, local, datum/language/language, message_range, volume_offset, list/listeners, pitch, force_blips = FALSE) - // MASSMETA EDIT END (ntts && /tg/tts) . = ..() src.identifier = identifier src.request = request @@ -744,8 +762,7 @@ SUBSYSTEM_DEF(tts) request_blips.begin_async() request_radio.begin_async() request_blips_radio.begin_async() - if(request_radio_gibberish) - request_radio_gibberish.begin_async() + request_radio_gibberish.begin_async() /datum/tts_request/proc/get_primary_request() if(local) @@ -781,12 +798,12 @@ SUBSYSTEM_DEF(tts) // MASSMETA EDIT END (ntts && /tg/tts) else var/datum/http_response/response = request.into_response() - var/datum/http_response/response_radio = request_radio.into_response() // MASSMETA EDIT START (ntts && /tg/tts) var/datum/http_response/response_blips = request_blips.into_response() + var/datum/http_response/response_radio = request_radio.into_response() var/datum/http_response/response_blips_radio = request_blips_radio.into_response() - return response.errored || response.status_code != 200 || response_blips.errored || response_blips.status_code != 200 || response_radio.errored || response_radio.status_code != 200 || response_blips_radio.errored || response_blips_radio.status_code != 200 - // MASSMETA EDIT END (ntts && /tg/tts) + var/datum/http_response/response_radio_gibberish = request_radio_gibberish.into_response() + return response.errored || response_blips.errored || response_radio.errored || response_blips_radio.errored || response_radio_gibberish.errored /datum/tts_request/proc/requests_completed() // MASSMETA EDIT START (ntts && /tg/tts) @@ -799,24 +816,27 @@ SUBSYSTEM_DEF(tts) else return request.is_complete() else - return request.is_complete() && request_blips.is_complete() && request_blips_radio.is_complete() && request_radio.is_complete() + return request.is_complete() && request_blips.is_complete() && request_blips_radio.is_complete() && request_radio.is_complete() && request_radio_gibberish.is_complete() /proc/filter_tts_listeners(list/listeners, radio_frequency = null) if(!SStts.tts_enabled || !listeners) return - if(ismob(listeners)) + if(isweakref(listeners)) listeners = list(listeners) var/list/filtered_listeners = list() - for(var/mob/listener as anything in listeners) - if(!ismob(listener) || !listener.client) + for(var/datum/weakref/listener as anything in listeners) + if(!isweakref(listener)) + continue + var/mob/possible_listener = listener?.resolve() + if(!ismob(possible_listener) || !possible_listener.client) continue - var/tts_pref = listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) - var/radio_tts_pref = listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts_radio) + var/tts_pref = possible_listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) + var/radio_tts_pref = possible_listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts_radio) if(tts_pref == TTS_SOUND_OFF) continue - if(isliving(listener) && (listener.stat >= UNCONSCIOUS || HAS_TRAIT(listener, TRAIT_DEAF))) + if(isliving(possible_listener) && (possible_listener.stat >= UNCONSCIOUS || HAS_TRAIT(possible_listener, TRAIT_DEAF))) continue if(radio_tts_pref == TTS_SOUND_NO_RADIO) continue diff --git a/code/datums/3d_sounds/_3d_sound.dm b/code/datums/3d_sounds/_3d_sound.dm index 280687c1f059..47424e0367dc 100644 --- a/code/datums/3d_sounds/_3d_sound.dm +++ b/code/datums/3d_sounds/_3d_sound.dm @@ -9,7 +9,6 @@ var/atom/parent var/sound/our_sound var/sound_path - var/list/mob/starting_listeners var/can_add_new_listeners = TRUE var/list/mob/listeners = list() var/volume = 50 @@ -51,18 +50,15 @@ z_cutoff = ceil(worldviewsize[2] / 2) for(var/listener in current_listeners) if(!ismob(listener)) - current_listeners -= current_listeners + stack_trace("[listener] found in current listeners list and is NOT A MOB!!!!!1! report this on github thx") + current_listeners -= listener continue register_listener(listener) - starting_listeners = current_listeners RegisterSignal(parent, COMSIG_ENTER_AREA, PROC_REF(on_enter_area)) RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(parent_delete)) - deletion_timer = addtimer(CALLBACK(src, PROC_REF(delete_self)), sound_length, TIMER_STOPPABLE | TIMER_DELETE_ME) - -/datum/threed_sound/proc/delete_self() - qdel(src) + deletion_timer = addtimer(CALLBACK(src, PROC_REF(selfdelete)), sound_length, TIMER_STOPPABLE | TIMER_DELETE_ME) /datum/threed_sound/Destroy() unlisten_all() @@ -73,7 +69,7 @@ /datum/threed_sound/proc/parent_delete(datum/source) SIGNAL_HANDLER - qdel(src) + selfdelete() /** * Sets the sound's range to a new value. This can be a number or a view size string "XxY". @@ -250,7 +246,9 @@ our_sound.x = new_x our_sound.z = new_z var/original_volume = our_sound.volume - var/calculated_volume = original_volume - CALCULATE_SOUND_VOLUME_RATIO(original_volume, get_dist(sound_turf, listener_turf), sound_range, falloff_distance, falloff_exponent) + var/distance = get_dist_euclidean(sound_turf, listener_turf) + var/dist_ratio = CALCULATE_SOUND_VOLUME_RATIO(original_volume, distance, sound_range, falloff_distance, falloff_exponent) + var/calculated_volume = original_volume - (dist_ratio * (original_volume - 5)) if(pressure_affected) //Atmosphere affects sound var/pressure_factor = 1 @@ -268,7 +266,7 @@ pressure_factor = max(pressure_factor, 0.15) //touching the source of the sound calculated_volume *= pressure_factor - if(calculated_volume < 3 || get_dist(sound_turf, listener_turf) > sound_range) + if(calculated_volume < 5 || get_dist(sound_turf, listener_turf) > sound_range) our_sound.volume = 0 else our_sound.volume = calculated_volume @@ -285,71 +283,3 @@ #undef MUTE_DEAF #undef MUTE_RANGE - -/obj/item/threed_sound_test - name = "fuck" - desc = "lmao" - icon = 'icons/obj/machines/music.dmi' - icon_state = "jukebox" - var/datum/threed_sound/threed_sound - var/our_channel - var/sound/new_sound - -/obj/item/threed_sound_test/Initialize(mapload) - . = ..() - var/list/listeners = get_hearers_in_view(7, src) - our_channel = SSsounds.random_available_channel() - new_sound = sound( - 'sound/machines/tram/other_line_processed.ogg', - FALSE, - 0, - our_channel, - 100 - ) - for(var/mob/listener in listeners) - listener.playsound_local( - turf_source = get_turf(src), - vol = 100, - vary = FALSE, - channel = our_channel, - sound_to_use = new_sound - ) - threed_sound = new( - src, - new_sound, - listeners, - FALSE, - 100, - 3, - 12 SECONDS, - our_channel, - /datum/preference/numeric/volume/sound_tts_volume, - COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED - ) - -/obj/item/threed_sound_test/attack_self(mob/user) - . = ..() - if(QDELETED(threed_sound)) - threed_sound = null - if(!threed_sound) - var/list/listeners = get_hearers_in_view(7, src) - for(var/mob/listener in listeners) - listener.playsound_local( - turf_source = get_turf(src), - vol = 100, - vary = FALSE, - channel = our_channel, - sound_to_use = new_sound - ) - threed_sound = new( - src, - new_sound, - listeners, - FALSE, - 100, - 3, - 12 SECONDS, - our_channel, - /datum/preference/numeric/volume/sound_tts_volume, - COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED - ) diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index dc14b7633cd6..520b2ae259a8 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -935,7 +935,6 @@ multiple modular subtrees with behaviors while(index <= length(remove_queue)) var/list/next_to_clear = remove_queue[index] for(var/inner_value in next_to_clear) - var/associated_value = next_to_clear[inner_value] // We are a lists of lists, add the next value to the queue so we can handle references in there // (But we only need to bother checking the list if it's not empty.) if(islist(inner_value) && length(inner_value)) @@ -945,6 +944,13 @@ multiple modular subtrees with behaviors else if(inner_value == source) next_to_clear -= inner_value + //if this is the case stop here. This means the list isn't associative (because an assoc list couldnt have a key for a number!) + if(isnum(inner_value)) + continue + + var/associated_value = next_to_clear[inner_value] + if(!associated_value) //This wasn't an associated list! we lied! its all been a trick. Try again next time. + continue // We are an assoc lists of lists, the list at the next value so we can handle references in there // (But again, we only need to bother checking the list if it's not empty.) if(islist(associated_value) && length(associated_value)) diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm index 1153833a31ee..a8980d15b851 100644 --- a/code/datums/ai/movement/ai_movement_jps.dm +++ b/code/datums/ai/movement/ai_movement_jps.dm @@ -12,6 +12,16 @@ var/atom/movable/moving = controller.pawn var/delay = controller.movement_delay + var/datum/move_loop/loop = setup_moveloop(controller, current_movement_target, moving, delay) + + RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) + RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) + RegisterSignal(loop, COMSIG_MOVELOOP_JPS_REPATH, PROC_REF(repath_incoming)) + + return loop + + +/datum/ai_movement/jps/proc/setup_moveloop(datum/ai_controller/controller, atom/current_movement_target, atom/movable/moving, delay) var/datum/move_loop/has_target/jps/loop = GLOB.move_manager.jps_move(moving, current_movement_target, delay, @@ -24,11 +34,6 @@ diagonal_handling = diagonal_flags, extra_info = controller, ) - - RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) - RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) - RegisterSignal(loop, COMSIG_MOVELOOP_JPS_REPATH, PROC_REF(repath_incoming)) - return loop /datum/ai_movement/jps/proc/repath_incoming(datum/move_loop/has_target/jps/source) @@ -47,9 +52,35 @@ var/datum/move_loop/loop = ..() var/atom/our_pawn = controller.pawn if(isnull(our_pawn)) - return + return null our_pawn.RegisterSignal(loop, COMSIG_MOVELOOP_JPS_FINISHED_PATHING, TYPE_PROC_REF(/mob/living/basic/bot, generate_bot_path)) + return loop /datum/ai_movement/jps/bot/travel_to_beacon maximum_length = AI_BOT_PATH_LENGTH max_pathing_attempts = 10 + + +/datum/ai_movement/jps/bot/mulebot + max_pathing_attempts = 10 + maximum_length = AI_MULEBOT_PATH_LENGTH + +/datum/ai_movement/jps/bot/mulebot/setup_moveloop(datum/ai_controller/controller, atom/current_movement_target, atom/movable/moving, delay) + var/datum/move_loop/has_target/jps/frustrations/loop = GLOB.move_manager.frustrations_move(moving, + current_movement_target, + delay, + repath_delay = 0.5 SECONDS, + simulated_only = !HAS_TRAIT(controller.pawn, TRAIT_SPACEWALK), + max_path_length = maximum_length, + minimum_distance = controller.get_minimum_distance(), + access = controller.get_access(), + subsystem = SSai_movement, + diagonal_handling = diagonal_flags, + extra_info = controller, + ) + return loop + +/datum/ai_movement/jps/bot/mulebot/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) + var/datum/move_loop/loop = ..() + var/atom/our_pawn = controller.pawn + our_pawn.RegisterSignal(loop, COMSIG_MOVELOOP_JPS_FRUSTRATION_INCREMENTED, TYPE_PROC_REF(/mob/living/basic/bot/mulebot, handle_buzzing)) diff --git a/code/datums/beam.dm b/code/datums/beam.dm index 9d5e7b6c35d3..b3debf2de38e 100644 --- a/code/datums/beam.dm +++ b/code/datums/beam.dm @@ -32,6 +32,8 @@ var/beam_color ///If we use an emissive appearance var/emissive = TRUE + /// If FALSE, redraws snap per update instead of using animate() interpolation. + var/animate = TRUE /// If set will be used instead of origin's pixel_x in offset calculations var/override_origin_pixel_x = null /// If set will be used instead of origin's pixel_y in offset calculations @@ -42,6 +44,34 @@ var/override_target_pixel_y = null ///the layer of our beam var/beam_layer + ///Whether we have a cached last-drawn geometry from a previous Draw(). + var/last_draw_valid = FALSE + ///Last drawn origin tile/pixel coordinates (used as the "from" frame for animated redraws). + var/last_origin_x = 0 + var/last_origin_y = 0 + var/last_origin_px = 0 + var/last_origin_py = 0 + ///Last drawn target tile/pixel coordinates. + var/last_target_x = 0 + var/last_target_y = 0 + var/last_target_px = 0 + var/last_target_py = 0 + ///Animate time queued for the pending redraw. We take the largest (slowest glide) of any movers that triggered the redraw. + var/pending_animate_time = 0 + ///Last animation's "from" (where segments were seeded at draw time) — origin endpoint. + var/anim_from_origin_x = 0 + var/anim_from_origin_y = 0 + var/anim_from_origin_px = 0 + var/anim_from_origin_py = 0 + ///Last animation's "from" — target endpoint. + var/anim_from_target_x = 0 + var/anim_from_target_y = 0 + var/anim_from_target_px = 0 + var/anim_from_target_py = 0 + ///world.time at which the last animation began. Combined with anim_duration to estimate segments' current visual position mid-animation. + var/anim_start_time = 0 + ///Duration of the last animation, in deciseconds (matches the time= passed to animate()). + var/anim_duration = 0 /datum/beam/New( origin, @@ -53,6 +83,7 @@ beam_type = /obj/effect/ebeam, beam_color = null, emissive = TRUE, + animate = TRUE, override_origin_pixel_x = null, override_origin_pixel_y = null, override_target_pixel_x = null, @@ -67,6 +98,7 @@ src.beam_type = beam_type src.beam_color = beam_color src.emissive = emissive + src.animate = animate src.override_origin_pixel_x = override_origin_pixel_x src.override_origin_pixel_y = override_origin_pixel_y src.override_target_pixel_x = override_target_pixel_x @@ -97,11 +129,59 @@ SIGNAL_HANDLER if(QDELING(src)) return - if(!QDELETED(origin) && !QDELETED(target) && get_dist(origin,target)= max_distance || origin.z != target.z) qdel(src) + return + var/queued_time = 0 + if(animate && istype(mover)) + queued_time = ICON_SIZE_ALL / max(mover.glide_size, MIN_GLIDE_SIZE) * world.tick_lag + if(queued_time > pending_animate_time) + pending_animate_time = queued_time + // Synchronous: deferring via INVOKE_ASYNC would start animate() one render frame after the mob's + // BYOND-managed glide, making the beam trail. Draw() doesn't sleep, so calling it here is safe. + Draw() + +/** Returns the last drawn endpoints for reuse by inherit_glide(), or null if undrawn. */ +/datum/beam/proc/get_last_geometry() + if(!last_draw_valid) + return null + return list( + "origin_x" = last_origin_x, + "origin_y" = last_origin_y, + "origin_px" = last_origin_px, + "origin_py" = last_origin_py, + "target_x" = last_target_x, + "target_y" = last_target_y, + "target_px" = last_target_px, + "target_py" = last_target_py, + ) + +/** Seeds the next Draw() from saved geometry so rebuilt beams glide instead of snapping. */ +/datum/beam/proc/inherit_glide(list/geometry, animate_time) + if(!geometry || animate_time <= 0) + return + last_origin_x = geometry["origin_x"] + last_origin_y = geometry["origin_y"] + last_origin_px = geometry["origin_px"] + last_origin_py = geometry["origin_py"] + last_target_x = geometry["target_x"] + last_target_y = geometry["target_y"] + last_target_px = geometry["target_px"] + last_target_py = geometry["target_py"] + // Mirror into the anim "from" frame; with anim_duration 0 the next Draw() treats progress as 1 and + // seeds segments exactly at these endpoints, then animates to the (new) live position. + anim_from_origin_x = last_origin_x + anim_from_origin_y = last_origin_y + anim_from_origin_px = last_origin_px + anim_from_origin_py = last_origin_py + anim_from_target_x = last_target_x + anim_from_target_y = last_target_y + anim_from_target_px = last_target_px + anim_from_target_py = last_target_py + anim_duration = 0 + anim_start_time = world.time + last_draw_valid = TRUE + pending_animate_time = animate_time /datum/beam/Destroy() QDEL_LIST(elements) @@ -115,54 +195,116 @@ /** * Creates the beam effects and places them in a line from the origin to the target. Sets their rotation to make the beams face the target, too. */ -/datum/beam/proc/Draw() +/datum/beam/proc/Draw(atom/movable/mover = null, atom/oldloc = null) if(SEND_SIGNAL(src, COMSIG_BEAM_BEFORE_DRAW) & BEAM_CANCEL_DRAW) return + var/animate_time = pending_animate_time + pending_animate_time = 0 + if(!animate) + animate_time = 0 var/origin_px = (isnull(override_origin_pixel_x) ? origin.pixel_x : override_origin_pixel_x) + origin.pixel_w var/origin_py = (isnull(override_origin_pixel_y) ? origin.pixel_y : override_origin_pixel_y) + origin.pixel_z var/target_px = (isnull(override_target_pixel_x) ? target.pixel_x : override_target_pixel_x) + target.pixel_w var/target_py = (isnull(override_target_pixel_y) ? target.pixel_y : override_target_pixel_y) + target.pixel_z - var/Angle = get_angle_raw(origin.x, origin.y, origin_px, origin_py, target.x , target.y, target_px, target_py) - ///var/Angle = round(get_angle(origin,target)) + + // Seed from where segments visually are *now*, not where the last Draw asked them to end up. + // If the previous animation is still in flight (e.g. consecutive-tick or mid-diagonal moves), + // using the cached destination teleports segments forward then animates back — the diagonal jump. + // Lerp last from→to by elapsed time to get the real current frame. + var/progress = 1 + if(last_draw_valid && anim_duration > 0) + progress = clamp((world.time - anim_start_time) / anim_duration, 0, 1) + var/old_origin_x_f = last_draw_valid ? (anim_from_origin_x + (last_origin_x - anim_from_origin_x) * progress) : origin.x + var/old_origin_y_f = last_draw_valid ? (anim_from_origin_y + (last_origin_y - anim_from_origin_y) * progress) : origin.y + var/old_origin_px_f = last_draw_valid ? (anim_from_origin_px + (last_origin_px - anim_from_origin_px) * progress) : origin_px + var/old_origin_py_f = last_draw_valid ? (anim_from_origin_py + (last_origin_py - anim_from_origin_py) * progress) : origin_py + var/old_target_x_f = last_draw_valid ? (anim_from_target_x + (last_target_x - anim_from_target_x) * progress) : target.x + var/old_target_y_f = last_draw_valid ? (anim_from_target_y + (last_target_y - anim_from_target_y) * progress) : target.y + var/old_target_px_f = last_draw_valid ? (anim_from_target_px + (last_target_px - anim_from_target_px) * progress) : target_px + var/old_target_py_f = last_draw_valid ? (anim_from_target_py + (last_target_py - anim_from_target_py) * progress) : target_py + if(!last_draw_valid) + animate_time = 0 + + // Endpoints in absolute world-pixel coordinates. + var/vector/origin_world = vector(origin.x * ICON_SIZE_X + origin_px, origin.y * ICON_SIZE_Y + origin_py) + var/vector/target_world = vector(target.x * ICON_SIZE_X + target_px, target.y * ICON_SIZE_Y + target_py) + var/vector/old_origin_world = vector(old_origin_x_f * ICON_SIZE_X + old_origin_px_f, old_origin_y_f * ICON_SIZE_Y + old_origin_py_f) + var/vector/old_target_world = vector(old_target_x_f * ICON_SIZE_X + old_target_px_f, old_target_y_f * ICON_SIZE_Y + old_target_py_f) + + var/Angle = get_angle_raw(origin.x, origin.y, origin_px, origin_py, target.x, target.y, target_px, target_py) + var/vector/beam_direction = vector(sin(Angle), cos(Angle)) + // Old angle from the interpolated endpoints. + var/vector/old_beam_delta = old_target_world - old_origin_world + var/OLD_DX_F = old_beam_delta.x + var/OLD_DY_F = old_beam_delta.y + var/old_angle + if(!OLD_DY_F) + old_angle = (OLD_DX_F >= 0) ? 90 : 270 + else + old_angle = arctan(OLD_DX_F / OLD_DY_F) + if(OLD_DY_F < 0) + old_angle += 180 + else if(OLD_DX_F < 0) + old_angle += 360 var/matrix/rot_matrix = matrix() + var/matrix/old_rot_matrix = matrix() var/turf/origin_turf = get_turf(origin) rot_matrix.Turn(Angle) - - //Translation vector for origin and target - var/DX = (32*target.x+target_px)-(32*origin.x+origin_px) - var/DY = (32*target.y+target_py)-(32*origin.y+origin_py) + old_rot_matrix.Turn(old_angle) + var/raw_angle_delta = abs(Angle - old_angle) + if(raw_angle_delta > 180) // Normalize to shortest-path angle across the 0/360 seam. + raw_angle_delta = 360 - raw_angle_delta + // Byond doesn't handle 180 degree rotations well + var/animate_rotation = animate_time && raw_angle_delta < 90 + + var/vector/beam_delta = target_world - origin_world + var/DX = beam_delta.x + var/DY = beam_delta.y var/N = 0 - var/length = round(sqrt((DX)**2+(DY)**2)) //hypotenuse of the triangle formed by target and origin's displacement + var/length = round(beam_delta.size) + var/old_length = round(old_beam_delta.size) + var/vector/old_beam_direction = vector(sin(old_angle), cos(old_angle)) + + var/list/old_elements = elements + var/list/new_elements = list() - for(N in 0 to length-1 step 32)//-1 as we want < not <=, but we want the speed of X in Y to Z and step X + for(N in 0 to length-1 step 32) if(QDELETED(src)) break + // Map each new segment to the same offset on the interpolated old beam. + var/old_pos = clamp(N + 16, 0, old_length) var/obj/effect/ebeam/segment = new beam_type(origin_turf, src) - elements += segment - - //ends are cropped by a transparent box icon of length-N pixel size laid over the visuals obj - if(N+32>length) //went past the target, we draw a box of space to cut away from the beam sprite so the icon actually ends at the center of the target sprite - var/icon/terminal_icon = new(icon, icon_state)//this means we exclude the overshooting object from the visual contents which does mean those visuals don't show up for the final bit of the beam... - terminal_icon.DrawBox(null,1,(length-N),32,32)//in the future if you want to improve this, remove the drawbox and instead use a 513 filter to cut away at the final object's icon + new_elements += segment + + var/icon/terminal_icon = null + if(N+32>length) + terminal_icon = new(icon, icon_state) + var/cut_row = length - N + terminal_icon.DrawBox(null, 1, cut_row, 32, 32) + // Soft alpha falloff so the tip isn't a hard line. + var/fade_height = min(4, cut_row - 1) + if(fade_height > 0) + var/icon/alpha_mask = new(icon, icon_state) + alpha_mask.DrawBox(rgb(255, 255, 255, 255), 1, 1, 32, 32) + var/band_start = cut_row - fade_height + for(var/y in band_start to cut_row - 1) + var/from_tip = (cut_row - 1) - y // 0 at the tip row, fade_height-1 furthest back + var/a = round(255 * (from_tip + 1) / (fade_height + 1), 1) + alpha_mask.DrawBox(rgb(255, 255, 255, a), 1, y, 32, y) + alpha_mask.DrawBox(null, 1, cut_row, 32, 32) + terminal_icon.Blend(alpha_mask, ICON_MULTIPLY) segment.icon = terminal_icon segment.color = beam_color else set_subsegment_appearance(segment) - segment.transform = rot_matrix - - //Calculate pixel offsets (If necessary) - var/Pixel_x - var/Pixel_y - if(DX == 0) - Pixel_x = 0 - else - Pixel_x = round(sin(Angle)+32*sin(Angle)*(N+16)/32) - if(DY == 0) - Pixel_y = 0 + if(animate_rotation) + segment.transform = old_rot_matrix else - Pixel_y = round(cos(Angle)+32*cos(Angle)*(N+16)/32) + segment.transform = rot_matrix + + var/Pixel_x = (DX == 0) ? 0 : round(sin(Angle) + 32 * sin(Angle) * (N + 16) / 32, 1) + var/Pixel_y = (DY == 0) ? 0 : round(cos(Angle) + 32 * cos(Angle) * (N + 16) / 32, 1) - //Position the effect so the beam is one continous line var/final_x = segment.x var/final_y = segment.y if(abs(Pixel_x)>32) @@ -171,11 +313,75 @@ if(abs(Pixel_y)>32) final_y += Pixel_y > 0 ? round(Pixel_y/32) : ceil(Pixel_y/32) Pixel_y %= 32 - segment.forceMove(locate(final_x, final_y, segment.z)) - segment.pixel_x = origin_px + Pixel_x - segment.pixel_y = origin_py + Pixel_y - CHECK_TICK + var/new_pixel_x = origin_px + Pixel_x + var/new_pixel_y = origin_py + Pixel_y + if(animate_time) + // Seed from interpolated old endpoints so consecutive redraws don't snap. + var/vector/old_visual = old_origin_world + old_beam_direction * old_pos + var/new_visual_x = final_x * ICON_SIZE_X + new_pixel_x + var/new_visual_y = final_y * ICON_SIZE_Y + new_pixel_y + segment.pixel_x = new_pixel_x + round(old_visual.x - new_visual_x, 1) + segment.pixel_y = new_pixel_y + round(old_visual.y - new_visual_y, 1) + // Segments past the old beam's end fade in instead of popping. + if(N >= old_length) + segment.alpha = 0 + animate(segment, alpha = 255, time = animate_time, flags = ANIMATION_PARALLEL) + if(animate_rotation) + animate(segment, pixel_x = new_pixel_x, pixel_y = new_pixel_y, transform = rot_matrix, time = animate_time, flags = ANIMATION_PARALLEL) + else + animate(segment, pixel_x = new_pixel_x, pixel_y = new_pixel_y, time = animate_time, flags = ANIMATION_PARALLEL) + else + segment.pixel_x = new_pixel_x + segment.pixel_y = new_pixel_y + if(emissive) + segment.add_overlay(emissive_appearance(terminal_icon ? terminal_icon : icon, terminal_icon ? "" : icon_state, segment, alpha = segment.alpha)) + + elements = new_elements + // Fade out extra segments before deleting them so shrinking the beam does not pop the tail. + var/old_count = length(old_elements) + var/new_count = length(new_elements) + if(animate_time && old_count > new_count && progress >= 1) + for(var/i in 1 to new_count) + qdel(old_elements[i]) + for(var/i in new_count + 1 to old_count) + var/obj/effect/ebeam/dying = old_elements[i] + // Project the dying segment onto the new beam and clamp it to the tip. + var/proj_pos = clamp((i - 1) * ICON_SIZE_ALL + 16, 0, length) + var/vector/proj_world = origin_world + beam_direction * proj_pos + var/dying_world_x = dying.x * ICON_SIZE_X + var/dying_world_y = dying.y * ICON_SIZE_Y + var/target_px_anim = round(proj_world.x - dying_world_x, 1) + var/target_py_anim = round(proj_world.y - dying_world_y, 1) + dying.cut_overlays() // Remove emissive overlay so it doesn't glow while the segment fades out. + if(animate_rotation) + animate(dying, pixel_x = target_px_anim, pixel_y = target_py_anim, transform = rot_matrix, alpha = 0, time = animate_time, flags = ANIMATION_PARALLEL) + else + animate(dying, pixel_x = target_px_anim, pixel_y = target_py_anim, alpha = 0, time = animate_time, flags = ANIMATION_PARALLEL) + QDEL_IN(dying, animate_time) + else + QDEL_LIST(old_elements) + + // Cache this draw's seed and destination so the next Draw() can lerp mid-animation. + anim_from_origin_x = old_origin_x_f + anim_from_origin_y = old_origin_y_f + anim_from_origin_px = old_origin_px_f + anim_from_origin_py = old_origin_py_f + anim_from_target_x = old_target_x_f + anim_from_target_y = old_target_y_f + anim_from_target_px = old_target_px_f + anim_from_target_py = old_target_py_f + last_origin_x = origin.x + last_origin_y = origin.y + last_origin_px = origin_px + last_origin_py = origin_py + last_target_x = target.x + last_target_y = target.y + last_target_px = target_px + last_target_py = target_py + anim_start_time = world.time + anim_duration = animate_time + last_draw_valid = TRUE /datum/beam/proc/set_up_effect(obj/effect/ebeam/beam_effect, effect_icon_state) beam_effect.icon = icon @@ -206,6 +412,7 @@ beam_type = /obj/effect/ebeam, beam_color = null, emissive = TRUE, + animate = TRUE, override_origin_pixel_x = null, override_origin_pixel_y = null, override_target_pixel_x = null, @@ -225,6 +432,7 @@ mouse_opacity = MOUSE_OPACITY_TRANSPARENT layer = ABOVE_ALL_MOB_LAYER anchored = TRUE + blocks_emissive = EMISSIVE_BLOCK_NONE var/emissive = TRUE var/datum/beam/owner @@ -232,15 +440,6 @@ owner = beam_owner return ..() -/obj/effect/ebeam/update_overlays() - . = ..() - if(!emissive) - return - var/mutable_appearance/emissive_overlay = emissive_appearance(icon, icon_state, src) - emissive_overlay.transform = transform - emissive_overlay.alpha = alpha - . += emissive_overlay - /obj/effect/ebeam/Destroy() owner = null return ..() @@ -331,18 +530,24 @@ time=INFINITY,maxdistance=INFINITY, beam_type=/obj/effect/ebeam, beam_color = null, emissive = TRUE, + animate = TRUE, override_origin_pixel_x = null, override_origin_pixel_y = null, override_target_pixel_x = null, override_target_pixel_y = null, layer = ABOVE_ALL_MOB_LAYER, icon_state_variants = 0, + glide_seed = null, + glide_time = 0, ) var/datum/beam/newbeam if(icon_state_variants <= 0) - newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer) + newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, animate, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer) else - newbeam = new /datum/beam/varied(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer, icon_state_variants) + newbeam = new /datum/beam/varied(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, animate, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer, icon_state_variants) + // Seed the glide before Start()'s first Draw() runs (INVOKE_ASYNC runs it synchronously here since + // Draw() never sleeps), so a rebuilt beam animates from its predecessor instead of snapping. + newbeam.inherit_glide(glide_seed, glide_time) INVOKE_ASYNC(newbeam, TYPE_PROC_REF(/datum/beam/, Start)) return newbeam diff --git a/code/datums/components/hide_weather_planes.dm b/code/datums/components/hide_weather_planes.dm index 7a7cb3655b8f..9c3270f5ac77 100644 --- a/code/datums/components/hide_weather_planes.dm +++ b/code/datums/components/hide_weather_planes.dm @@ -8,20 +8,17 @@ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS var/list/datum/weather/active_weather = list() var/list/atom/movable/screen/plane_master/plane_masters = list() - /// Do we care about all weather or only particle weather? - var/particle_only = FALSE -/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about, particle_only = FALSE) +/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about) if(!istype(parent, /datum/plane_master_group)) return COMPONENT_INCOMPATIBLE - src.particle_only = particle_only var/datum/plane_master_group/home = parent plane_masters += care_about RegisterSignal(care_about, COMSIG_QDELETING, PROC_REF(plane_master_deleted)) var/list/starting_signals = list() var/list/ending_signals = list() - for(var/datum/weather/weather_type as anything in valid_subtypesof(particle_only ? /datum/weather/particle : /datum/weather)) + for(var/datum/weather/weather_type as anything in valid_subtypesof(/datum/weather)) starting_signals += COMSIG_WEATHER_TELEGRAPH(weather_type) ending_signals += COMSIG_WEATHER_END(weather_type) @@ -106,8 +103,6 @@ var/list/connected_levels = SSmapping.get_connected_levels(new_z) for(var/datum/weather/active as anything in SSweather.processing) - if(particle_only && !istype(active, /datum/weather/particle)) - continue if(length(connected_levels & active.impacted_z_levels)) active_weather += WEAKREF(active) diff --git a/code/datums/components/listen_prayers.dm b/code/datums/components/listen_prayers.dm new file mode 100644 index 000000000000..d22724eb6666 --- /dev/null +++ b/code/datums/components/listen_prayers.dm @@ -0,0 +1,89 @@ +/** + * A component that allows the attached mind to listen prayers from other mobs. + * Unlike admins, they cannot smite the person for it, also the prayers are anonymous unless the person says who he's in the message. + */ +/datum/component/listen_prayers + ///A callback called before the prayer is heard. May alter the message or prevent it from being heard if it returns FALSE + var/datum/callback/pre_prayer_callback + ///This will be affixed to the "deities" that have heard this message. + var/deity_name + ///The action that the owner can use to ignore the prayers + var/datum/action/innate/listen_prayers/toggle + ///The description we give to the instance of the listen_prayers toggle. + var/toggle_desc + +/datum/component/listen_prayers/Initialize(datum/callback/pre_prayer_callback, deity_name, toggle_desc) + if (!istype(parent, /datum/mind)) + return COMPONENT_INCOMPATIBLE + src.pre_prayer_callback = pre_prayer_callback + src.deity_name = deity_name + src.toggle_desc = toggle_desc + +/datum/component/listen_prayers/Destroy() + pre_prayer_callback = null + return ..() + +/datum/component/listen_prayers/RegisterWithParent() + RegisterSignal(SSdcs, COMSIG_GLOB_SEND_PRAYER, PROC_REF(on_sent_prayer)) + var/datum/mind/mind = parent + + var/datum/action/innate/listen_prayers/toggle = new(mind) + if(toggle_desc) + toggle.desc = toggle_desc + if(mind.current) + toggle.Grant(mind.current) + +/datum/component/listen_prayers/UnregisterFromParent() + UnregisterSignal(SSdcs, COMSIG_GLOB_SEND_PRAYER) + QDEL_NULL(toggle) + +/datum/component/listen_prayers/proc/on_sent_prayer(source, mob/praying, message, prayer_type, symbol, list/deities_that_listened) + SIGNAL_HANDLER + var/datum/mind/mind = parent + if(!mind.current || mind.current.stat >= UNCONSCIOUS || !mind.current.client) //You can't hear prayers if unconscious or disconnected + return + if(!isliving(praying) || praying.stat == DEAD) + return FALSE //I don't see any reason in hell to why dead people should be allowed into this. This isn't a knockoff TRAIT_SIXTHSENSE. + if(praying == mind.current) //Ignore prayers coming from ourselves. + return + if(mind.current.client in GLOB.admins) //This is redundant if we're adminning + return + if(HAS_MIND_TRAIT(mind.current, TRAIT_DONT_HEAR_PRAYERS)) + return + var/list/arguments = args.Copy(2) + if(pre_prayer_callback && !pre_prayer_callback.Invoke(args)) + return + prayer_type = arguments[ARG_PRAYER_TYPE] + symbol = arguments[ARG_PRAYER_SYMBOL] + message = "[icon2html(arguments[ARG_PRAYER_SYMBOL], mind.current.client)][prayer_type]: [span_notice(arguments[ARG_PRAYER_MSG])]" + to_chat(mind.current, custom_boxed_message(GLOB.prayer_type_to_message_box[prayer_type], message)) + SEND_SOUND(mind.current, sound('sound/effects/pray.ogg')) + if(deity_name) + deities_that_listened += deity_name + +///An action to stop hearing prayers on command. Doesn't do a whole lot without the associated component +/datum/action/innate/listen_prayers + name = "Listen Prayers" + button_icon = 'icons/hud/actions.dmi' + button_icon_state = "pray" + desc = "Allows you to eavesdrop on prayers from the world around you." + active = TRUE + +/datum/action/innate/listen_prayers/Destroy() + REMOVE_TRAIT(owner, TRAIT_DONT_HEAR_PRAYERS, ACTION_TRAIT) + return ..() + +/datum/action/innate/listen_prayers/is_action_active(atom/movable/screen/movable/action_button/current_button) + return !HAS_TRAIT_FROM(owner, TRAIT_DONT_HEAR_PRAYERS, ACTION_TRAIT) + +/datum/action/innate/listen_prayers/Activate() + active = TRUE + REMOVE_TRAIT(owner, TRAIT_DONT_HEAR_PRAYERS, ACTION_TRAIT) + to_chat(owner, span_green("You are ready to listen to prayers once again.")) + build_all_button_icons(UPDATE_BUTTON_BACKGROUND) + +/datum/action/innate/listen_prayers/Deactivate() + active = FALSE + ADD_TRAIT(owner, TRAIT_DONT_HEAR_PRAYERS, ACTION_TRAIT) + to_chat(owner, span_green("You stop listening to prayers.")) + build_all_button_icons(UPDATE_BUTTON_BACKGROUND) diff --git a/code/datums/elements/uses_girder_wall_recipes.dm b/code/datums/elements/uses_girder_wall_recipes.dm index b313f78200e9..38593f89f54d 100644 --- a/code/datums/elements/uses_girder_wall_recipes.dm +++ b/code/datums/elements/uses_girder_wall_recipes.dm @@ -82,6 +82,8 @@ structure.add_fingerprint(user) stack.add_fingerprint(user) + // Save refernce to the materials for the case when we place last tile in the stack + var/list/saved_mats_per_unit = stack.mats_per_unit if (!stack.use_tool(structure, user, recipe.make_delay, recipe.stack_amount, extra_checks = CALLBACK(src, PROC_REF(check_recipe), structure, user, recipe))) return @@ -103,7 +105,7 @@ qdel(structure) if (is_material_recipe) - wall.set_custom_materials(stack.mats_per_unit, recipe.stack_amount) + wall.set_custom_materials(saved_mats_per_unit, recipe.stack_amount) /// Checks if the user can do the wall recipe. /datum/element/uses_girder_wall_recipes/proc/check_recipe(obj/structure/structure, mob/living/user, datum/girder_wall_recipe/recipe) diff --git a/code/datums/elements/waddling.dm b/code/datums/elements/waddling.dm index d89504dbcfec..2831e729cd75 100644 --- a/code/datums/elements/waddling.dm +++ b/code/datums/elements/waddling.dm @@ -18,7 +18,7 @@ return if(isliving(moved)) var/mob/living/living_moved = moved - if (living_moved.incapacitated || (living_moved.body_position == LYING_DOWN && !HAS_TRAIT(living_moved, TRAIT_FLOPPING))) + if (living_moved.buckled || living_moved.incapacitated || (living_moved.body_position == LYING_DOWN && !HAS_TRAIT(living_moved, TRAIT_FLOPPING))) return waddling_animation(moved) diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm index 2f0b0c030a06..9bdb31c3561c 100644 --- a/code/datums/emotes.dm +++ b/code/datums/emotes.dm @@ -61,12 +61,18 @@ var/cooldown = 0.8 SECONDS /// Does this message have a message that can be modified by the user? var/can_message_change = FALSE - /// How long is the shared emote cooldown triggered by this emote? - var/general_emote_audio_cooldown = 2 SECONDS - /// How long is the specific emote cooldown triggered by this emote? - var/specific_emote_audio_cooldown = 5 SECONDS + /// How long is the shared emote cooldown triggered by this emote when used intentionally? + var/manual_general_emote_audio_cooldown = 2 SECONDS + /// How long is the specific emote cooldown triggered by this emote when used intentionally? + var/manual_specific_emote_audio_cooldown = 5 SECONDS + /// How long is the shared emote cooldown triggered by this emote when forced? + var/forced_general_emote_audio_cooldown = 2 SECONDS + /// How long is the specific emote cooldown triggered by this emote when forced? + var/forced_specific_emote_audio_cooldown = 2 SECONDS /// Does this emote's sound ignore walls? var/sound_wall_ignore = FALSE + ///Does this emote use sound tokens? this means it also ignores walls. + var/use_sound_tokens = FALSE /datum/emote/New() switch(mob_type_allowed_typecache) @@ -112,15 +118,27 @@ user.log_message(msg, LOG_EMOTE) var/tmp_sound = get_sound(user) - if(tmp_sound && should_play_sound(user, intentional) && TIMER_COOLDOWN_FINISHED(user, "general_emote_audio_cooldown") && TIMER_COOLDOWN_FINISHED(user, type)) - TIMER_COOLDOWN_START(user, type, specific_emote_audio_cooldown) - TIMER_COOLDOWN_START(user, "general_emote_audio_cooldown", general_emote_audio_cooldown) + if(tmp_sound && should_play_sound(user, intentional)) + if(intentional) + if(!TIMER_COOLDOWN_FINISHED(user, MANUAL_GENERAL_EMOTE_AUDIO_COOLDOWN) || !TIMER_COOLDOWN_FINISHED(user, MANUAL_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type)) || !TIMER_COOLDOWN_FINISHED(user, FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN) || !TIMER_COOLDOWN_FINISHED(user, FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type))) + return FALSE + else + if(!TIMER_COOLDOWN_FINISHED(user, FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN) || !TIMER_COOLDOWN_FINISHED(user, FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type))) + return FALSE + TIMER_COOLDOWN_START(user, MANUAL_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type), manual_specific_emote_audio_cooldown) + TIMER_COOLDOWN_START(user, MANUAL_GENERAL_EMOTE_AUDIO_COOLDOWN, manual_general_emote_audio_cooldown) + TIMER_COOLDOWN_START(user, FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type), forced_specific_emote_audio_cooldown) + TIMER_COOLDOWN_START(user, FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN, forced_general_emote_audio_cooldown) + var/frequency = null if (affected_by_pitch && SStts.tts_enabled && SStts.pitch_enabled) frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH) * (1 + sqrt(abs(user.pitch)) * sign(user.pitch) * EMOTE_TTS_PITCH_MULTIPLIER) else if(vary) frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH) - playsound(source = user,soundin = tmp_sound,vol = 50, vary = FALSE, ignore_walls = sound_wall_ignore, frequency = frequency) + if(use_sound_tokens && sound_wall_ignore) + playsoundtoken(source = user, soundin = tmp_sound, range = SOUND_RANGE, volume = 50) + else + playsound(source = user,soundin = tmp_sound,vol = 50, vary = FALSE, ignore_walls = sound_wall_ignore, frequency = frequency) var/is_important = running_emote_type & EMOTE_IMPORTANT diff --git a/code/datums/map_config.dm b/code/datums/map_config.dm index c3793f141e5c..8734190b57ce 100644 --- a/code/datums/map_config.dm +++ b/code/datums/map_config.dm @@ -34,8 +34,10 @@ var/wilderness_levels = 0 /// Directory to the wilderness area we can spawn in var/wilderness_directory + /// Z-Level traits our wilderness maps will get, ice box traits by default + var/list/wilderness_z_traits = ZTRAITS_ICY_WILDS /// Index of map names (inside wilderness_directory) with the amount to spawn. ("ice_planes" = 1) for one ice spawn - var/list/maps_to_spawn = list() + var/list/wilderness_maps_to_spawn = list() ///The type of mining Z-level that should be loaded. var/minetype = MINETYPE_LAVALAND @@ -259,8 +261,12 @@ // Just pick and take based on weight for(var/i in 1 to wilderness_levels) - maps_to_spawn += pick_weight_take(wilderness) - shuffle(maps_to_spawn) + wilderness_maps_to_spawn += pick_weight_take(wilderness) + shuffle(wilderness_maps_to_spawn) + + var/list/wilderness_level_traits = json["wilderness_level_traits"] + if (islist(wilderness_level_traits)) + wilderness_z_traits = wilderness_level_traits #ifdef UNIT_TESTS // Check for unit tests to skip, no reason to check these if we're not running tests diff --git a/code/datums/material/material_container.dm b/code/datums/materials/material_container/material_container.dm similarity index 100% rename from code/datums/material/material_container.dm rename to code/datums/materials/material_container/material_container.dm diff --git a/code/datums/material/remote_materials.dm b/code/datums/materials/material_container/remote_materials.dm similarity index 100% rename from code/datums/material/remote_materials.dm rename to code/datums/materials/material_container/remote_materials.dm diff --git a/code/datums/shuttles/arrival.dm b/code/datums/shuttles/arrival.dm index 38cb67fa1d79..b020295b5fb2 100644 --- a/code/datums/shuttles/arrival.dm +++ b/code/datums/shuttles/arrival.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/arrival port_id = "arrival" + prefix = "_maps/shuttles/arrival/" who_can_purchase = null /datum/map_template/shuttle/arrival/box diff --git a/code/datums/shuttles/aux_base.dm b/code/datums/shuttles/aux_base.dm index c377e278b906..1f6600456bd1 100644 --- a/code/datums/shuttles/aux_base.dm +++ b/code/datums/shuttles/aux_base.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/aux_base port_id = "aux_base" + prefix = "_maps/shuttles/aux_base/" who_can_purchase = null /datum/map_template/shuttle/aux_base/default diff --git a/code/datums/shuttles/cargo.dm b/code/datums/shuttles/cargo.dm index b3fadeaaa75b..ba0e1bfd223f 100644 --- a/code/datums/shuttles/cargo.dm +++ b/code/datums/shuttles/cargo.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/cargo port_id = "cargo" + prefix = "_maps/shuttles/cargo/" name = "Base Shuttle Template (Cargo)" who_can_purchase = null diff --git a/code/datums/shuttles/emergency.dm b/code/datums/shuttles/emergency.dm index 6197afc9a3f8..aeb896e54dea 100644 --- a/code/datums/shuttles/emergency.dm +++ b/code/datums/shuttles/emergency.dm @@ -3,6 +3,7 @@ /datum/map_template/shuttle/emergency port_id = "emergency" name = "Base Shuttle Template (Emergency)" + prefix = "_maps/shuttles/emergency/" ///assoc list of shuttle events to add to this shuttle on spawn (typepath = weight) var/list/events ///pick all events instead of random diff --git a/code/datums/shuttles/escape_pod.dm b/code/datums/shuttles/escape_pod.dm index 0b2f35dd9dd4..250e08415409 100644 --- a/code/datums/shuttles/escape_pod.dm +++ b/code/datums/shuttles/escape_pod.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/escape_pod port_id = "escape_pod" + prefix = "_maps/shuttles/escape_pod/" who_can_purchase = null /datum/map_template/shuttle/escape_pod/default diff --git a/code/datums/shuttles/ferry.dm b/code/datums/shuttles/ferry.dm index f9c5841abf32..aa874f1b19be 100644 --- a/code/datums/shuttles/ferry.dm +++ b/code/datums/shuttles/ferry.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/ferry port_id = "ferry" + prefix = "_maps/shuttles/ferry/" name = "Base Shuttle Template (Ferry)" /datum/map_template/shuttle/ferry/base diff --git a/code/datums/shuttles/hunter.dm b/code/datums/shuttles/hunter.dm index ade978c937e0..140388398ea2 100644 --- a/code/datums/shuttles/hunter.dm +++ b/code/datums/shuttles/hunter.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/hunter port_id = "hunter" + prefix = "_maps/shuttles/hunter/" who_can_purchase = null /datum/map_template/shuttle/hunter/space_cop diff --git a/code/datums/shuttles/infiltrator.dm b/code/datums/shuttles/infiltrator.dm index 414dcb5129fe..a892736048b5 100644 --- a/code/datums/shuttles/infiltrator.dm +++ b/code/datums/shuttles/infiltrator.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/infiltrator port_id = "infiltrator" + prefix = "_maps/shuttles/infiltrator/" who_can_purchase = null /datum/map_template/shuttle/infiltrator/basic diff --git a/code/datums/shuttles/labour.dm b/code/datums/shuttles/labour.dm new file mode 100644 index 000000000000..e8bbdecaeba2 --- /dev/null +++ b/code/datums/shuttles/labour.dm @@ -0,0 +1,24 @@ +/datum/map_template/shuttle/labour + port_id = "labour" + prefix = "_maps/shuttles/labour/" + who_can_purchase = null + +/datum/map_template/shuttle/labour/box + suffix = "box" + name = "labour shuttle (Box)" + +/datum/map_template/shuttle/labour/generic + suffix = "generic" + name = "labour shuttle (Generic)" + +/datum/map_template/shuttle/labour/delta + suffix = "delta" + name = "labour shuttle (Delta)" + +/datum/map_template/shuttle/labour/kilo + suffix = "kilo" + name = "labour shuttle (Kilo)" + +/datum/map_template/shuttle/labour/nebula + suffix = "nebula" + name = "labour shuttle (Nebula)" diff --git a/code/datums/shuttles/mining.dm b/code/datums/shuttles/mining.dm index 21747a488e32..276797b5f524 100644 --- a/code/datums/shuttles/mining.dm +++ b/code/datums/shuttles/mining.dm @@ -1,31 +1,7 @@ -// LABOUR SHUTTLES -/datum/map_template/shuttle/labour - port_id = "labour" - who_can_purchase = null - -/datum/map_template/shuttle/labour/box - suffix = "box" - name = "labour shuttle (Box)" - -/datum/map_template/shuttle/labour/generic - suffix = "generic" - name = "labour shuttle (Generic)" - -/datum/map_template/shuttle/labour/delta - suffix = "delta" - name = "labour shuttle (Delta)" - -/datum/map_template/shuttle/labour/kilo - suffix = "kilo" - name = "labour shuttle (Kilo)" - -/datum/map_template/shuttle/labour/nebula - suffix = "nebula" - name = "labour shuttle (Nebula)" - // MINING SHUTTLES /datum/map_template/shuttle/mining port_id = "mining" + prefix = "_maps/shuttles/mining/" who_can_purchase = null /datum/map_template/shuttle/mining/box @@ -55,6 +31,7 @@ // MINING COMMON SHUTTLES /datum/map_template/shuttle/mining_common port_id = "mining_common" + prefix = "_maps/shuttles/mining/" who_can_purchase = null /datum/map_template/shuttle/mining_common/meta diff --git a/code/datums/shuttles/pirate.dm b/code/datums/shuttles/pirate.dm index 99f866d23fc6..62c6e97f37a6 100644 --- a/code/datums/shuttles/pirate.dm +++ b/code/datums/shuttles/pirate.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/pirate port_id = "pirate" + prefix = "_maps/shuttles/pirate/" who_can_purchase = null /datum/map_template/shuttle/pirate/default diff --git a/code/datums/shuttles/ruin.dm b/code/datums/shuttles/ruin.dm index 511e2d6ecdc8..1163909458db 100644 --- a/code/datums/shuttles/ruin.dm +++ b/code/datums/shuttles/ruin.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/ruin port_id = "ruin" + prefix = "_maps/shuttles/ruin/" who_can_purchase = null /datum/map_template/shuttle/ruin/cyborg_mothership diff --git a/code/datums/shuttles/starfury.dm b/code/datums/shuttles/starfury.dm index 510033d64369..85bf00b97c91 100644 --- a/code/datums/shuttles/starfury.dm +++ b/code/datums/shuttles/starfury.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/starfury port_id = "starfury" + prefix = "_maps/shuttles/starfury/" who_can_purchase = null /datum/map_template/shuttle/starfury/fighter_one diff --git a/code/datums/shuttles/whiteship.dm b/code/datums/shuttles/whiteship.dm index b8dbcfd4bd76..86743a25afc8 100644 --- a/code/datums/shuttles/whiteship.dm +++ b/code/datums/shuttles/whiteship.dm @@ -1,5 +1,6 @@ /datum/map_template/shuttle/whiteship port_id = "whiteship" + prefix = "_maps/shuttles/whiteship/" /datum/map_template/shuttle/whiteship/box suffix = "box" diff --git a/code/datums/sound_token.dm b/code/datums/sound_token.dm index 93fdfb8213bb..1eedde7c85e4 100644 --- a/code/datums/sound_token.dm +++ b/code/datums/sound_token.dm @@ -6,7 +6,8 @@ var/atom/source /// k:v list of mob : sound status var/list/listeners = list() - + ///k:v list of mobs : bool. Used to quickly check whether a mob is allowed to hear this noise. This is null by default which means ANY MOB can hear this. + var/list/allowed_listeners /// Sound maximum range var/range /// Sound volume @@ -29,10 +30,14 @@ var/start_time /// Duration of the current sound file in deciseconds. Used to wrap offset for looping sounds. var/sound_duration + /// Duration of the current sound file in deciseconds. Used to wrap offset for looping sounds. + var/sound_duration_override /// Cell tracker managing spatial grid cells within range of the source. The wizards say this is the fastest. var/datum/cell_tracker/cell_tracker + ///Should we destroy the datum when the sound is done? + var/delete_on_end = FALSE -/datum/sound_token/New(atom/_source, _sound, _range = 10, _volume = 50, _falloff_exponent = SOUND_FALLOFF_EXPONENT, _falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE) +/datum/sound_token/New(atom/_source, _sound, _range = 10, _volume = 50, _falloff_exponent = SOUND_FALLOFF_EXPONENT, _falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, _allowed_listeners, _sound_duration_override, _delete_on_end) source = _source RegisterSignal(source, COMSIG_QDELETING, PROC_REF(source_deleted)) RegisterSignal(source, COMSIG_MOVABLE_MOVED, PROC_REF(source_moved)) @@ -42,6 +47,13 @@ volume = _volume falloff_exponent = _falloff_exponent falloff_distance = _falloff_distance + sound_duration_override = _sound_duration_override + if(_delete_on_end) + delete_on_end = _delete_on_end + + if(_allowed_listeners) + for(var/allowed_mob in _allowed_listeners) + allowed_listeners[allowed_mob] = TRUE update_sound(_sound) @@ -53,6 +65,7 @@ RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGIN, PROC_REF(player_login)) RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGOUT, PROC_REF(player_logout)) + /datum/sound_token/Destroy(force, ...) for(var/listener in listeners) remove_listener(listener) @@ -67,29 +80,37 @@ if(!sound_channel) sound_channel = SSsounds.reserve_sound_channel_for_datum(src) sound.channel = sound_channel - sound_duration = SSsounds.get_sound_length(_sound) + sound_duration = sound_duration_override || SSsounds.get_sound_length(_sound) start_time = REALTIMEOFDAY if(start_playing) force_update_all_listeners(FALSE) + if(delete_on_end) + addtimer(CALLBACK(src, PROC_REF(on_sound_ended)), sound_duration, TIMER_UNIQUE | TIMER_OVERRIDE) + /// Updates the data of a listener, or adds them if they are not present. /datum/sound_token/proc/add_or_update_listener(mob/listener_mob) if(isnull(listeners[listener_mob])) - add_listener(listener_mob) + if(!add_listener(listener_mob)) + return FALSE else update_listener(listener_mob) -/// Adds a listener to the sound. +/// Adds a listener to the sound. returns TRUE if we already were added, or for some reason couldnt be added. /datum/sound_token/proc/add_listener(mob/listener_mob) if(!isnull(listeners[listener_mob])) - return FALSE + return TRUE if(!listener_mob.client || isnewplayer(listener_mob)) - return + return FALSE + + if(allowed_listeners && !allowed_listeners[listener_mob]) + return FALSE listeners[listener_mob] = NONE listener_mob.client.sound_tokens += src - RegisterSignal(listener_mob, COMSIG_QDELETING, PROC_REF(listener_deleted)) + if(source != listener_mob) //this is possible...yea... :/ + RegisterSignal(listener_mob, COMSIG_QDELETING, PROC_REF(listener_deleted)) RegisterSignals(listener_mob, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deafness_update)) update_listener(listener_mob, FALSE) return TRUE @@ -282,3 +303,7 @@ if(!still_in_range) remove_listener(listener_mob) + +///The sound should have ended on all clients. Time to destroy the sound token. +/datum/sound_token/proc/on_sound_ended() + qdel(src) diff --git a/code/datums/sprite_accessories/clothing.dm b/code/datums/sprite_accessories/clothing.dm index 4a021674c24b..d28c0086dc88 100644 --- a/code/datums/sprite_accessories/clothing.dm +++ b/code/datums/sprite_accessories/clothing.dm @@ -50,7 +50,7 @@ result = mutable_appearance(created) else // no caching necessary - result = mutable_appearance(icon, icon_state) + result = mutable_appearance(icon, icon_state_to_use) result.layer = -layer result.color = use_static ? null : color diff --git a/code/datums/status_effects/cuffed_item.dm b/code/datums/status_effects/cuffed_item.dm index 297666efc75a..53ccad34b143 100644 --- a/code/datums/status_effects/cuffed_item.dm +++ b/code/datums/status_effects/cuffed_item.dm @@ -323,7 +323,7 @@ var/atom/movable/movable_parent = parent link_effect = movable_parent.AddComponent(/datum/component/leash, owner = chained_to, distance = 1) tug_effect = movable_parent.AddComponent(/datum/component/tug_towards, tugging_to = chained_to, strength = 0.66) - beam_effect = movable_parent.Beam(chained_to, "chain") + beam_effect = movable_parent.Beam(chained_to, "chain", animate = FALSE) RegisterSignal(link_effect, COMSIG_QDELETING, PROC_REF(delete_self)) RegisterSignal(tug_effect, COMSIG_QDELETING, PROC_REF(delete_self)) RegisterSignal(beam_effect, COMSIG_QDELETING, PROC_REF(recreate_beam)) @@ -345,7 +345,7 @@ UnregisterSignal(beam_effect, COMSIG_QDELETING) var/atom/movable/movable_parent = parent - beam_effect = movable_parent.Beam(chained_to_weakref.resolve(), "chain") + beam_effect = movable_parent.Beam(chained_to_weakref.resolve(), "chain", animate = FALSE) RegisterSignal(beam_effect, COMSIG_QDELETING, PROC_REF(recreate_beam)) /datum/component/chained_together/proc/delete_self(datum/source) diff --git a/code/datums/status_effects/debuffs/slime/slime_leech.dm b/code/datums/status_effects/debuffs/slime/slime_leech.dm index 5c7b7567d553..d1125b2d25d5 100644 --- a/code/datums/status_effects/debuffs/slime/slime_leech.dm +++ b/code/datums/status_effects/debuffs/slime/slime_leech.dm @@ -54,7 +54,7 @@ SEND_SIGNAL(owner, COMSIG_SLIME_DRAINED, our_slime) - if(prob(60) && owner.client && ishuman(owner) && !our_slime.ai_controller.blackboard[BB_SLIME_RABID]) + if(prob(60) && ishuman(owner) && owner.client && !our_slime.ai_controller.blackboard[BB_SLIME_RABID]) our_slime.ai_controller?.set_blackboard_key(BB_SLIME_RABID, TRUE) //we might go rabid after finishing to feed on a human with a client. our_slime.stop_feeding() diff --git a/code/datums/storage/storage_interface.dm b/code/datums/storage/storage_interface.dm index 2e42e34a6ea0..2fb7929b0ab4 100644 --- a/code/datums/storage/storage_interface.dm +++ b/code/datums/storage/storage_interface.dm @@ -150,28 +150,18 @@ var/current_y = screen_start_y var/turf/our_turf = get_turf(real_location) - for(var/i in 1 to length(usable_modules)) - var/atom/movable/item = usable_modules[i] - if(item in robot_model.robot.held_items) - current_x++ - if(current_x - screen_start_x < columns) - continue - current_x = screen_start_x - - current_y++ - if(current_y - screen_start_y >= rows) - break - //Module is currently active - continue - - item.mouse_opacity = MOUSE_OPACITY_OPAQUE - SET_PLANE(item, ABOVE_HUD_PLANE, our_turf) - item.screen_loc = "[current_x]:[screen_pixel_x],[current_y]:[screen_pixel_y]" + for(var/obj/item/item in usable_modules) + //Only for non active modules + if(item.item_flags & IN_STORAGE) + item.mouse_opacity = MOUSE_OPACITY_OPAQUE + SET_PLANE(item, ABOVE_HUD_PLANE, our_turf) + item.screen_loc = "[current_x]:[screen_pixel_x + item.base_pixel_x],[current_y]:[screen_pixel_y + item.base_pixel_y]" current_x++ if(current_x - screen_start_x < columns) continue current_x = screen_start_x + current_y++ if(current_y - screen_start_y >= rows) break diff --git a/code/datums/votes/map_vote.dm b/code/datums/votes/map_vote.dm index 08f792d291de..dd099919aeb0 100644 --- a/code/datums/votes/map_vote.dm +++ b/code/datums/votes/map_vote.dm @@ -4,14 +4,13 @@ count_method = VOTE_COUNT_METHOD_SINGLE winner_method = VOTE_WINNER_METHOD_NONE display_statistics = FALSE + /datum/vote/map_vote/New() . = ..() default_choices = SSmap_vote.get_valid_map_vote_choices() /datum/vote/map_vote/create_vote() - var/list/new_choices = SSmap_vote.get_valid_map_vote_choices() - if (new_choices) - default_choices = new_choices + default_choices = SSmap_vote.get_valid_map_vote_choices() . = ..() if(!.) return FALSE @@ -43,10 +42,13 @@ if(SSmap_vote.next_map_config) return "The next map has already been selected." + // The below case will be caught in create_vote() if the vote is being forced + // This ensures proper map rotation if there aren't enough votable maps for whatever reason + if(forced) + return VOTE_AVAILABLE + var/list/new_choices = SSmap_vote.get_valid_map_vote_choices() - if (new_choices) - default_choices = new_choices - var/num_choices = length(default_choices) + var/num_choices = length(new_choices) if(num_choices <= 1) return "There [num_choices == 1 ? "is only one map" : "are no maps"] to choose from." diff --git a/code/datums/weather/particle_weather.dm b/code/datums/weather/particle_weather.dm index b8e1a5ac3fe2..1c7f385b092b 100644 --- a/code/datums/weather/particle_weather.dm +++ b/code/datums/weather/particle_weather.dm @@ -16,6 +16,8 @@ /// How often can we change our severity? /// Don't set this too low or it'll look jank var/severity_cooldown = 5 SECONDS + /// Alpha of the area overlay when the particle weather pref is enabled + var/particle_weather_alpha = 255 /// Current weather severity var/severity = 0 @@ -85,7 +87,7 @@ new_severity = clamp(new_severity, min(new_severity, min_severity), max_severity) if (WIND_DOWN_STAGE) // Slowly goes down to zero - new_severity += rand() * -severity_variation * (severity / min_severity) + new_severity += rand() * -severity_variation * 4 * max(severity / min_severity, 1) animate_severity(new_severity) @@ -101,6 +103,15 @@ /datum/weather/particle/generate_overlay_cache() . = ..() + + if (stage == END_STAGE) + return + + // Change alpha of overlays on the particle weather plane + for(var/mutable_appearance/overlay as anything in .) + if (PLANE_TO_TRUE(overlay.plane) == PARTICLE_WEATHER_PLANE) + overlay.alpha = particle_weather_alpha + for (var/offset in 0 to SSmapping.max_plane_offset) . += mutable_appearance('icons/effects/weather_overlay.dmi', "weather_overlay", overlay_layer, null, WEATHER_MASK_PLANE, offset_const = offset) diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm index de0d5cfc0577..362607442415 100644 --- a/code/datums/weather/weather.dm +++ b/code/datums/weather/weather.dm @@ -76,8 +76,9 @@ /// Since it's above everything else, this is the layer used by default. var/overlay_layer = AREA_LAYER - /// Plane for the overlay - var/overlay_plane = WEATHER_PLANE + /// Planes for the overlay + /// Base visuals should always render to both particle and non-particle planes as to work regardless of the toggle + var/list/overlay_planes = list(WEATHER_PLANE, PARTICLE_WEATHER_PLANE) /// Used by mobs (or movables containing mobs, such as enviro bags) to prevent them from being affected by the weather. var/immunity_type /// If this bit of weather should also draw an overlay that's uneffected by lighting onto the area @@ -586,9 +587,12 @@ glow_overlay.color = weather_color gen_overlay_cache += glow_overlay - var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, alpha = weather_alpha, offset_const = offset) - new_weather_overlay.color = weather_color - gen_overlay_cache += new_weather_overlay + // By default we render ourselves to both particle and non-particle weather, as those are mutually exclusive + // So that particle weather can have full alpha overlays when the pref is disabled, but partially transparent overlays when its enabled + for (var/overlay_plane in overlay_planes) + var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, alpha = weather_alpha, offset_const = offset) + new_weather_overlay.color = weather_color + gen_overlay_cache += new_weather_overlay return gen_overlay_cache diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm index ae12115e367f..16d04c8e19b6 100644 --- a/code/datums/weather/weather_types/ash_storm.dm +++ b/code/datums/weather/weather_types/ash_storm.dm @@ -7,7 +7,7 @@ emissive_type = /particles/weather/ash_storm/embers min_severity = 60 optimal_severity = 80 - weather_alpha = 100 + particle_weather_alpha = 100 wind_sign = -1 // Always blows left to sync with the animated overlays telegraph_message = span_boldwarning("An eerie moan rises on the wind. Sheets of burning ash blacken the horizon. Seek shelter.") diff --git a/code/datums/weather/weather_types/floor_is_lava.dm b/code/datums/weather/weather_types/floor_is_lava.dm index 2dcbbed0aa75..536c3461a2ba 100644 --- a/code/datums/weather/weather_types/floor_is_lava.dm +++ b/code/datums/weather/weather_types/floor_is_lava.dm @@ -19,7 +19,7 @@ target_trait = ZTRAIT_STATION overlay_layer = ABOVE_OPEN_TURF_LAYER //Covers floors only - overlay_plane = FLOOR_PLANE + overlay_planes = list(FLOOR_PLANE) immunity_type = TRAIT_LAVA_IMMUNE /// We don't draw on walls, so this ends up lookin weird /// Can't really use like, the emissive system here because I am not about to make diff --git a/code/datums/wires/mulebot.dm b/code/datums/wires/mulebot.dm index c3b4efe3460e..fa22771945d2 100644 --- a/code/datums/wires/mulebot.dm +++ b/code/datums/wires/mulebot.dm @@ -3,7 +3,7 @@ #define SLOW_MOTOR_SPEED 3 /datum/wires/mulebot - holder_type = /mob/living/simple_animal/bot/mulebot + holder_type = /mob/living/basic/bot/mulebot proper_name = "Mulebot" randomize = TRUE @@ -19,12 +19,12 @@ /datum/wires/mulebot/interactable(mob/user) if(!..()) return FALSE - var/mob/living/simple_animal/bot/mulebot/mule = holder - if(mule.bot_cover_flags & BOT_COVER_MAINTS_OPEN) + var/mob/living/basic/bot/mulebot/mule = holder + if(mule.bot_access_flags & BOT_COVER_MAINTS_OPEN) return TRUE /datum/wires/mulebot/on_cut(wire, mend, source) - var/mob/living/simple_animal/bot/mulebot/mule = holder + var/mob/living/basic/bot/mulebot/mule = holder switch(wire) if(WIRE_MOTOR1, WIRE_MOTOR2) if(is_cut(WIRE_MOTOR1) && is_cut(WIRE_MOTOR2)) @@ -49,7 +49,7 @@ holder.audible_message(span_hear("Something inside [mule] clicks ominously!"), null, 1) /datum/wires/mulebot/on_pulse(wire) - var/mob/living/simple_animal/bot/mulebot/mule = holder + var/mob/living/basic/bot/mulebot/mule = holder if(!mule.has_power(TRUE)) return //logically mulebots can't flash and beep if they don't have power. switch(wire) @@ -57,7 +57,7 @@ holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] The charge light flickers.")) if(WIRE_AVOIDANCE) holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] The external warning lights flash briefly.")) - flick("[mule.base_icon]1", mule) + flick("[mule.base_icon_state]1", mule) if(WIRE_LOADCHECK) holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] The load platform clunks.")) if(WIRE_MOTOR1, WIRE_MOTOR2) @@ -65,6 +65,7 @@ else holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] You hear a radio crackle.")) + #undef FAST_MOTOR_SPEED #undef AVERAGE_MOTOR_SPEED #undef SLOW_MOTOR_SPEED diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm index f56633befce7..a38ba6847583 100644 --- a/code/game/data_huds.dm +++ b/code/game/data_huds.dm @@ -455,46 +455,6 @@ Diagnostic HUDs! else set_hud_image_state(DIAG_CAMERA_HUD, "hudcamera") -/*~~~~~~~~~ - Bots! -~~~~~~~~~~*/ -/mob/living/simple_animal/bot/proc/diag_hud_set_bothealth() - set_hud_image_state(DIAG_HUD, "huddiag[RoundDiagBar(health/maxHealth)]") - -/mob/living/simple_animal/bot/proc/diag_hud_set_botstat() //On (With wireless on or off), Off, EMP'ed - if(bot_mode_flags & BOT_MODE_ON) - set_hud_image_state(DIAG_STAT_HUD, "hudstat") - else if(stat) //Generally EMP causes this - set_hud_image_state(DIAG_STAT_HUD, "hudoffline") - else //Bot is off - set_hud_image_state(DIAG_STAT_HUD, "huddead2") - -/mob/living/simple_animal/bot/proc/diag_hud_set_botmode() //Shows a bot's current operation - if(client) //If the bot is player controlled, it will not be following mode logic! - set_hud_image_state(DIAG_BOT_HUD, "hudsentient") - return - - switch(mode) - if(BOT_SUMMON, BOT_RESPONDING) //Responding to PDA or AI summons - set_hud_image_state(DIAG_BOT_HUD, "hudcalled") - if(BOT_CLEANING, BOT_HEALING) //Cleanbot cleaning, repairbot fixing, or Medibot Healing - set_hud_image_state(DIAG_BOT_HUD, "hudworking") - if(BOT_PATROL, BOT_START_PATROL) //Patrol mode - set_hud_image_state(DIAG_BOT_HUD, "hudpatrol") - if(BOT_PREP_ARREST, BOT_ARREST, BOT_HUNT) //STOP RIGHT THERE, CRIMINAL SCUM! - set_hud_image_state(DIAG_BOT_HUD, "hudalert") - if(BOT_MOVING, BOT_DELIVER, BOT_GO_HOME, BOT_NAV) //Moving to target for normal bots, moving to deliver or go home for MULES. - set_hud_image_state(DIAG_BOT_HUD, "hudmove") - else - set_hud_image_state(DIAG_BOT_HUD, "") - -/mob/living/simple_animal/bot/mulebot/proc/diag_hud_set_mulebotcell() - if(QDELETED(cell) || (cell.maxcharge == 0)) - set_hud_image_state(DIAG_BATT_HUD, "hudnobatt") - else - var/chargelvl = (cell.charge/cell.maxcharge) - set_hud_image_state(DIAG_BATT_HUD, "hudbatt[RoundDiagBar(chargelvl)]") - /*~~~~~~~~~~~~ Airlocks! ~~~~~~~~~~~~~*/ diff --git a/code/game/machinery/autolathe.dm b/code/game/machinery/autolathe.dm index 3343bd9416e1..247b729f1e94 100644 --- a/code/game/machinery/autolathe.dm +++ b/code/game/machinery/autolathe.dm @@ -210,11 +210,10 @@ /obj/machinery/autolathe/ui_data(mob/user) var/list/data = list() - data["materials"] = list() + data["materials"] = materials.ui_data() data["materialtotal"] = materials.total_amount() data["materialsmax"] = materials.max_amount data["active"] = busy - data["materials"] = materials.ui_data() return data @@ -223,6 +222,27 @@ if(.) return + if (action == "eject") + var/datum/material/material = locate(params["ref"]) + if(!istype(material)) + return + + var/amount = params["amount"] + if(isnull(amount)) + return + + amount = text2num(amount) + if(isnull(amount)) + return + + //we use initial(active_power_usage) because higher tier parts will have higher active usage but we have no benefit from it + if(!directly_use_energy(ROUND_UP((amount / MAX_STACK_SIZE) * 0.4 * initial(active_power_usage)))) + say("No power to dispense sheets") + return + + materials.retrieve_stack(amount, material) + return TRUE + //sanity checks to start printing if(action != "make") stack_trace("unknown autolathe ui_act: [action]") diff --git a/code/game/machinery/botlaunchpad.dm b/code/game/machinery/botlaunchpad.dm index 857e9278af01..ab949fb43585 100644 --- a/code/game/machinery/botlaunchpad.dm +++ b/code/game/machinery/botlaunchpad.dm @@ -65,12 +65,8 @@ user.balloon_alert(user, "no bots sent from the pad!") return user.balloon_alert(user, "bot sent back to pad") - if(isbasicbot(our_bot)) - var/mob/living/basic/bot/basic_bot = our_bot - basic_bot.summon_bot(src) - return - var/mob/living/simple_animal/bot/simple_bot = our_bot - simple_bot.call_bot(src, get_turf(src)) + var/mob/living/basic/bot/basic_bot = our_bot + basic_bot.summon_bot(src) /obj/structure/closet/supplypod/transport/botpod reverse_option_list = list("Mobs"=TRUE,"Objects"=FALSE,"Anchored"=FALSE,"Underfloor"=FALSE,"Wallmounted"=FALSE,"Floors"=FALSE,"Walls"=FALSE,"Mecha"=FALSE) diff --git a/code/game/machinery/lightswitch.dm b/code/game/machinery/lightswitch.dm index 7d553e47b2d6..4091e012985a 100644 --- a/code/game/machinery/lightswitch.dm +++ b/code/game/machinery/lightswitch.dm @@ -15,6 +15,10 @@ var/light_on_range = 1 /// Should this lightswitch automatically rename itself to match the area it's in? var/autoname = TRUE + /// The sound the light makes when it's turned on + var/sound_on = 'sound/items/weapons/magin.ogg' + /// The sound the light makes when it's turned off + var/sound_off = 'sound/items/weapons/magout.ogg' MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/light_switch, 26) @@ -73,6 +77,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/light_switch, 26) /obj/machinery/light_switch/interact(mob/user) . = ..() + playsound(src, area.lightswitch ? sound_off : sound_on, 40, TRUE) set_lights(!area.lightswitch) /obj/machinery/light_switch/screwdriver_act(mob/living/user, obj/item/tool) diff --git a/code/game/machinery/machine_frame.dm b/code/game/machinery/machine_frame.dm index 008849404eb1..5f7cf10d55f5 100644 --- a/code/game/machinery/machine_frame.dm +++ b/code/game/machinery/machine_frame.dm @@ -445,7 +445,7 @@ user.balloon_alert(user, "missing components!") return FALSE - if(!circuit.completion_requirements(src)) + if(!circuit.completion_requirements(src, user)) return FALSE tool.play_tool_sound(src) diff --git a/code/game/machinery/wall_healer.dm b/code/game/machinery/wall_healer.dm index 4789b59d390e..a865477563e7 100644 --- a/code/game/machinery/wall_healer.dm +++ b/code/game/machinery/wall_healer.dm @@ -1,3 +1,5 @@ +#define WALL_HEALER_OFFSET 32 + /// A wall mounted machine that heals chip damage for a price /obj/machinery/wall_healer name = "\improper DeForest first aid station" @@ -10,6 +12,7 @@ payment_department = ACCOUNT_MED max_integrity = 150 armor_type = /datum/armor/obj_machinery/wall_healer + circuit = /obj/item/circuitboard/machine/wall_healer /// Cost per bandage dispensed. Note, always disregarded on red alert. var/per_bandage_cost = (/obj/item/stack/medical/wrap/gauze::custom_price) / (/obj/item/stack/medical/wrap/gauze::amount) @@ -51,26 +54,29 @@ VAR_PRIVATE/antispam_counter = 0 /datum/armor/obj_machinery/wall_healer - melee = 50 - bullet = 30 + melee = 20 + bullet = 20 laser = 30 - energy = 40 + energy = 30 bomb = 10 fire = 80 acid = 80 + bio = 100 /obj/machinery/wall_healer/Initialize(mapload) . = ..() if(!mapload) + num_bandages = 0 brute_healing = 0 burn_healing = 0 tox_healing = 0 blood_healing = 0 update_appearance() + if(istype(circuit) && (circuit.obj_flags & EMAGGED)) + obj_flags |= EMAGGED init_payment() register_context() - if(mapload) - find_and_mount_on_atom() + find_and_mount_on_atom() /obj/machinery/wall_healer/Destroy() clear_using_mob() @@ -85,6 +91,30 @@ if(istype(held_item, /obj/item/stack/medical/wrap/gauze)) context[SCREENTIP_CONTEXT_LMB] = "Restock" return CONTEXTUAL_SCREENTIP_SET + if(held_item?.tool_behaviour == TOOL_SCREWDRIVER) + context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel" + return CONTEXTUAL_SCREENTIP_SET + if(held_item?.tool_behaviour == TOOL_CROWBAR && can_crowbar_deconstruct()) + context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" + return CONTEXTUAL_SCREENTIP_SET + return NONE + +// Someone please add generic support for constructing wall mounted objects thanks +/obj/machinery/wall_healer/on_construction(mob/user) + if(user.dir & NORTH) + pixel_y += WALL_HEALER_OFFSET + else if(user.dir & SOUTH) + pixel_y -= WALL_HEALER_OFFSET + + if(user.dir & EAST) + pixel_x += WALL_HEALER_OFFSET + else if(user.dir & WEST) + pixel_x -= WALL_HEALER_OFFSET + + if(!find_and_mount_on_atom()) + stack_trace("Got to on_construction for [type], but failed to mount on a wall! This should be asserted from construction requirements.") + loc.balloon_alert(user, "no wall to install on!") + deconstruct(TRUE) /obj/machinery/wall_healer/proc/refill_healing_pool(percent = 100) var/amount_refilled = 0 @@ -126,6 +156,9 @@ /obj/machinery/wall_healer/update_overlays() . = ..() + if(panel_open) + . += "open" + var/brute_state = 7 - round(7 * (brute_healing / initial(brute_healing)), 1) var/mutable_appearance/brute = mutable_appearance(icon, "bar[brute_state]", alpha = src.alpha, appearance_flags = RESET_COLOR) brute.color = /datum/reagent/medicine/c2/libital::color @@ -162,8 +195,21 @@ visible_message(span_warning("Sparks fly out of [src]!")) balloon_alert(user, "safeties disabled") obj_flags |= EMAGGED + circuit?.obj_flags |= EMAGGED return TRUE +/obj/machinery/wall_healer/screwdriver_act(mob/living/user, obj/item/tool) + return screwdriver_act_secondary(user, tool) + +/obj/machinery/wall_healer/screwdriver_act_secondary(mob/living/user, obj/item/tool) + return default_deconstruction_screwdriver(user, tool) + +/obj/machinery/wall_healer/crowbar_act(mob/living/user, obj/item/tool) + return crowbar_act_secondary(user, tool) + +/obj/machinery/wall_healer/crowbar_act_secondary(mob/living/user, obj/item/tool) + return default_deconstruction_crowbar(user, tool) + /// We want user to be right up to the wall mount to use it /// However people may often map the machine over a table /// In those contexts, they should be allowed to reach over the table @@ -354,7 +400,8 @@ var/atom/drop_loc = drop_location() for(var/obj/item/stack/medical/wrap/gauze/bandage as anything in stocked_bandages) bandage.forceMove(drop_loc) - new /obj/item/stack/medical/wrap/gauze(drop_loc, num_bandages) + if(num_bandages > 0) + new /obj/item/stack/medical/wrap/gauze(drop_loc, num_bandages) /obj/machinery/wall_healer/item_interaction(mob/living/user, obj/item/tool, list/modifiers) if(!istype(tool, /obj/item/stack/medical/wrap/gauze)) @@ -556,12 +603,17 @@ update(COOLDOWN_FINISHED(healer, injection_cooldown) ? 0 : COOLDOWN_TIMELEFT(healer, injection_cooldown)) -MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/wall_healer, 32) +MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/wall_healer, WALL_HEALER_OFFSET) /obj/machinery/wall_healer/free name = "\improper DeForest emergency first aid station" + circuit = /obj/item/circuitboard/machine/wall_healer/free + recharge_cd_length = 60 SECONDS + injection_cd_length = 2 SECONDS /obj/machinery/wall_healer/free/init_payment() return -MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/wall_healer/free, 32) +MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/wall_healer/free, WALL_HEALER_OFFSET) + +#undef WALL_HEALER_OFFSET diff --git a/code/game/objects/items/circuitboards/circuitboard.dm b/code/game/objects/items/circuitboards/circuitboard.dm index 40e85008cd40..5c97a173cb2f 100644 --- a/code/game/objects/items/circuitboards/circuitboard.dm +++ b/code/game/objects/items/circuitboards/circuitboard.dm @@ -69,7 +69,7 @@ * Arguments: * * install_frame - The frame the circuit has been installed into for reference. */ -/obj/item/circuitboard/proc/completion_requirements(obj/structure/frame/install_frame) +/obj/item/circuitboard/proc/completion_requirements(obj/structure/frame/install_frame, mob/living/user) return TRUE // Circuitboard/machine diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm index 774bbc2765e4..9465ecdcf29d 100644 --- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm @@ -36,7 +36,7 @@ /datum/stock_part/servo/tier3 = 5, /obj/item/stack/cable_coil = 2) -/obj/item/circuitboard/machine/dna_vault/completion_requirements(obj/structure/frame/install_frame) +/obj/item/circuitboard/machine/dna_vault/completion_requirements(obj/structure/frame/install_frame, mob/living/user) var/turf/center = get_turf(install_frame) var/blocked = FALSE for(var/turf/potential_turf as anything in CORNER_BLOCK_OFFSET(center, 3, 3, -1, -2)) @@ -1948,3 +1948,46 @@ /datum/stock_part/water_recycler = 1, /datum/stock_part/servo = 1, ) + +/obj/item/circuitboard/machine/wall_healer + name = "DeForest First Aid Station" + greyscale_colors = CIRCUIT_COLOR_MEDICAL + build_path = /obj/machinery/wall_healer + req_components = list( + /obj/item/healthanalyzer/simple = 1, + /obj/item/reagent_containers/syringe = 1, + /obj/item/hemostat = 1, + /obj/item/scalpel = 1, + ) + +/obj/item/circuitboard/machine/wall_healer/examine(mob/user) + . = ..() + if(obj_flags & EMAGGED) + . += span_warning("The safety chip looks fried.") + +/obj/item/circuitboard/machine/wall_healer/emag_act(mob/user, obj/item/card/emag/emag_card) + if(obj_flags & EMAGGED) + return FALSE + + playsound(src, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + visible_message(span_warning("Sparks fly out of [src]!")) + balloon_alert(user, "safeties disabled") + obj_flags |= EMAGGED + return TRUE + +// Someone please add generic support for constructing wall mounted objects thanks +/obj/item/circuitboard/machine/wall_healer/completion_requirements(obj/structure/frame/install_frame, mob/living/user) + if(locate(/obj/machinery/wall_healer) in install_frame.loc) // for subtypes support + install_frame.balloon_alert(user, "identical machine present!") + return FALSE + + var/turf/facing_wall = get_step(install_frame, user.dir) + if(!is_mountable_turf(facing_wall)) + install_frame.balloon_alert(user, "no wall to install on!") + return FALSE + + return TRUE + +/obj/item/circuitboard/machine/wall_healer/free + name = "DeForest Emergency First Aid Station" + build_path = /obj/machinery/wall_healer/free diff --git a/code/game/objects/items/devices/earthcracker.dm b/code/game/objects/items/devices/earthcracker.dm index 455be3168f26..5faf68aad3df 100644 --- a/code/game/objects/items/devices/earthcracker.dm +++ b/code/game/objects/items/devices/earthcracker.dm @@ -134,7 +134,7 @@ /// Cleanup after an earthcracker is activated either for sabotage or mining. /obj/item/earthcracker/proc/handle_after_activation(turf/cracked_hull) do_sparks(2, FALSE, src) - cracked_hull.levelupdate() + cracked_hull?.levelupdate() status = EARTHCRACKER_SPENT update_appearance(UPDATE_ICON) diff --git a/code/game/objects/items/robot/items/tools.dm b/code/game/objects/items/robot/items/tools.dm index 351949715451..0060a502cef7 100644 --- a/code/game/objects/items/robot/items/tools.dm +++ b/code/game/objects/items/robot/items/tools.dm @@ -191,16 +191,8 @@ if (!issilicon(user)) return - var/mob/living/silicon/robot/as_cyborg = user - if (!(src in as_cyborg.held_items)) - context[SCREENTIP_CONTEXT_RMB] = "Select Tool" - return CONTEXTUAL_SCREENTIP_SET - -/obj/item/borg/cyborg_omnitool/examine(mob/user) - . = ..() - if(reference) - var/obj/item/tool = get_proxy_attacker_for(src, usr) - . += tool.examine(user) + context[SCREENTIP_CONTEXT_RMB] = "Select Tool" + return CONTEXTUAL_SCREENTIP_SET /** * Sets the new internal tool to be used @@ -209,6 +201,8 @@ * * obj/item/ref - typepath for the new internal omnitool */ /obj/item/borg/cyborg_omnitool/proc/set_internal_tool(obj/item/tool) + SHOULD_NOT_OVERRIDE(TRUE) + for(var/obj/item/internal_tool as anything in omni_toolkit) if(internal_tool == tool) reference = internal_tool @@ -247,6 +241,8 @@ //the internal tool is considered part of the tool itself, so don't let it be dropped. tool.item_flags |= ABSTRACT ADD_TRAIT(tool, TRAIT_NODROP, INNATE_TRAIT) + //assign the upgraded toolspeed, if any. + tool.toolspeed = initial(tool.toolspeed) - upgraded * 0.3 //store tool for future use atoms[reference] = tool @@ -256,17 +252,19 @@ //build the radial menu options var/list/radial_menu_options = list() var/list/tool_map = list() - for(var/obj/item as anything in omni_toolkit) - var/tool_name = initial(item.name) - radial_menu_options[tool_name] = image(icon = initial(item.icon), icon_state = initial(item.icon_state)) - tool_map[tool_name] = item + for(var/obj/item/tool as anything in omni_toolkit) + if(initial(tool.tool_behaviour) == tool_behaviour) + continue + var/tool_name = initial(tool.name) + radial_menu_options[tool_name] = image(icon = initial(tool.icon), icon_state = initial(tool.icon_state)) + tool_map[tool_name] = tool //assign the new tool behaviour var/internal_tool_name = show_radial_menu(user, src, radial_menu_options, require_near = TRUE, tooltips = TRUE) if(!internal_tool_name) return - //set the reference & update icons + //set the reference and update appearance set_internal_tool(tool_map[internal_tool_name]) update_appearance(UPDATE_ICON_STATE) playsound(src, 'sound/items/tools/change_jaws.ogg', 50, TRUE) @@ -276,8 +274,7 @@ if(!LAZYACCESS(modifiers, RIGHT_CLICK) || !iscyborg(usr)) return ..() var/mob/living/silicon/robot/user = usr - if (!(src in user.held_items)) - attack_self(user, modifiers) + attack_self(user, modifiers) return ..() /obj/item/borg/cyborg_omnitool/update_icon_state() @@ -301,7 +298,6 @@ /obj/item/borg/cyborg_omnitool/medical name = "surgical omni-toolset" desc = "A set of surgical tools used by cyborgs to operate on various surgical operations." - omni_toolkit = list( /obj/item/surgical_drapes/cyborg, /obj/item/scalpel/cyborg, @@ -319,74 +315,26 @@ desc = "A set of engineering tools used by cyborgs to conduct various engineering tasks." icon = 'icons/obj/items_cyborg.dmi' icon_state = "toolkit_engiborg" - omni_toolkit = list( /obj/item/wrench/cyborg, /obj/item/wirecutters/cyborg, /obj/item/screwdriver/cyborg, /obj/item/crowbar/cyborg, /obj/item/multitool/cyborg, - /obj/item/weldingtool/largetank/cyborg, ) -/obj/item/borg/cyborg_omnitool/engineering/Initialize(mapload) - . = ..() - RegisterSignal(src, COMSIG_SILICON_MODULE_ACTIVATION, PROC_REF(welder_toggle)) - -/obj/item/borg/cyborg_omnitool/engineering/update_overlays() +/obj/item/borg/cyborg_omnitool/engineering/examine(mob/user) . = ..() - if(tool_behaviour == TOOL_WELDER) - var/obj/item/weldingtool/tool = atoms[/obj/item/weldingtool/largetank/cyborg] - if(tool?.welding) - . |= tool.update_overlays() - -/obj/item/borg/cyborg_omnitool/engineering/attack_self(mob/user, modifiers) - if(tool_behaviour == TOOL_WELDER && LAZYACCESS(modifiers, LEFT_CLICK)) - welder_toggle(src, null, user) - - return NONE - - return ..() - -/obj/item/borg/cyborg_omnitool/engineering/set_internal_tool(obj/item/tool) - if(tool_behaviour == TOOL_WELDER) - welder_toggle(src, FALSE) - - return ..() - -///Reflects internal welder icon onto the omnitool -/obj/item/borg/cyborg_omnitool/engineering/proc/welder_update(source) - PRIVATE_PROC(TRUE) - SIGNAL_HANDLER - - update_appearance(UPDATE_OVERLAYS) - -///Toggles welder on/off when module slot is selected/deselected -/obj/item/borg/cyborg_omnitool/engineering/proc/welder_toggle(datum/omnitool, state, mob/self_user) - PRIVATE_PROC(TRUE) - SIGNAL_HANDLER - - if(tool_behaviour == TOOL_WELDER) - var/obj/item/weldingtool/tool = get_proxy_attacker_for(src, usr) - if(isnull(state)) - state = !tool.welding - if(state == tool.welding) - return - - if(state) - RegisterSignal(tool, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(welder_update), override = TRUE) - if(self_user) - tool.switched_on(self_user) - else - tool.switched_off() - UnregisterSignal(tool, COMSIG_ATOM_UPDATE_APPEARANCE) + if(tool_behaviour == TOOL_MULTITOOL) + var/obj/item/multitool/tool = atoms[/obj/item/multitool/cyborg] + if(tool?.buffer) + . += span_notice("Multitool buffer contains [tool.buffer].") /obj/item/borg/cyborg_omnitool/botany name = "botanical omni-toolset" desc = "A set of botanical tools used by cyborgs to do gardening." icon = 'icons/obj/items_cyborg.dmi' icon_state = "sili" - omni_toolkit = list( /obj/item/secateurs/cyborg, /obj/item/cultivator/cyborg, @@ -394,6 +342,5 @@ /obj/item/shovel/spade/cyborg, ) - #undef PKBORG_DAMPEN_CYCLE_DELAY #undef POWER_RECHARGE_CYBORG_DRAIN_MULTIPLIER diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm index f608845936a8..f2d4b782d6cf 100644 --- a/code/game/objects/items/robot/robot_upgrades.dm +++ b/code/game/objects/items/robot/robot_upgrades.dm @@ -485,6 +485,8 @@ return for(var/obj/item/borg/cyborg_omnitool/engineering/omnitool in cyborg.model.modules) omnitool.set_upgraded(TRUE) + for(var/obj/item/weldingtool/largetank/cyborg/welder in cyborg.model.modules) + welder.toolspeed = initial(welder.toolspeed) - 0.3 /obj/item/borg/upgrade/engineering_omnitool/deactivate(mob/living/silicon/robot/cyborg, mob/living/user = usr) . = ..() @@ -492,6 +494,8 @@ return for(var/obj/item/borg/cyborg_omnitool/engineering/omnitool in cyborg.model.modules) omnitool.set_upgraded(FALSE) + for(var/obj/item/weldingtool/largetank/cyborg/welder in cyborg.model.modules) + welder.toolspeed = initial(welder.toolspeed) /obj/item/borg/upgrade/defib name = "medical cyborg defibrillator" diff --git a/code/game/objects/items/stacks/tiles/tile_types.dm b/code/game/objects/items/stacks/tiles/tile_types.dm index c8f2c59d7c32..915fbf2af5e5 100644 --- a/code/game/objects/items/stacks/tiles/tile_types.dm +++ b/code/game/objects/items/stacks/tiles/tile_types.dm @@ -1162,9 +1162,11 @@ merge_type = /obj/item/stack/tile/material /obj/item/stack/tile/material/place_tile(turf/open/target_plating, mob/user) + // Save refernce to the materials for the case when we place last tile in the stack + var/list/saved_mats_per_unit = mats_per_unit . = ..() var/turf/open/floor/material/floor = . - floor?.set_custom_materials(mats_per_unit) + floor?.set_custom_materials(saved_mats_per_unit) /obj/item/stack/tile/eighties name = "retro tile" diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm index 545d74127738..7923e3919a52 100644 --- a/code/game/objects/structures/crates_lockers/crates.dm +++ b/code/game/objects/structures/crates_lockers/crates.dm @@ -236,11 +236,17 @@ base_icon_state = "medicalcrate" /obj/structure/closet/crate/deforest - name = "deforest medical crate" + name = "\improper DeForest Medical crate" desc = "A DeForest brand crate of medical supplies." icon_state = "deforest" base_icon_state = "deforest" +/obj/structure/closet/crate/interdyne_normal + name = "\improper Interdyne Pharmaceutics crate" + desc = "An Interdyne Pharmaceutics brand crate. Probably contains helpful chemicals? Hopefully contains helpful chemicals." + icon_state = "interdynecrate" + base_icon_state = "interdynecrate" + /obj/structure/closet/crate/medical/department icon_state = "medical" base_icon_state = "medical" diff --git a/code/game/objects/structures/lavaland/ore_vent.dm b/code/game/objects/structures/lavaland/ore_vent.dm index 3ab9542f6301..11f8009b6103 100644 --- a/code/game/objects/structures/lavaland/ore_vent.dm +++ b/code/game/objects/structures/lavaland/ore_vent.dm @@ -555,7 +555,7 @@ Shake(duration = 1.5 SECONDS) //decorate the boulder with materials - var/list/mats_list = list() + var/list/mats_list = new_rock.custom_materials?.Copy() || list() for(var/iteration in 1 to MINERALS_PER_BOULDER) var/datum/material/material = pick_weight(mineral_breakdown) mats_list[material] += ore_quantity_function(iteration) diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm index 0c153c59128e..816418009682 100644 --- a/code/game/objects/structures/plasticflaps.dm +++ b/code/game/objects/structures/plasticflaps.dm @@ -224,7 +224,7 @@ else if(isliving(mover)) // You Shall Not Pass! var/mob/living/living_mover = mover - if(istype(living_mover.buckled, /mob/living/simple_animal/bot/mulebot)) // mulebot passenger gets a free pass. + if(istype(living_mover.buckled, /mob/living/basic/bot/mulebot)) // mulebot passenger gets a free pass. return TRUE if(living_mover.body_position == STANDING_UP && living_mover.mob_size != MOB_SIZE_TINY && !(HAS_TRAIT(living_mover, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(living_mover, TRAIT_VENTCRAWLER_NUDE))) diff --git a/code/game/say.dm b/code/game/say.dm index e988e484dd12..f7db8e01737d 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -70,7 +70,7 @@ GLOBAL_LIST_INIT(freqtospan, list( /// Returns TRUE if the message was received and understood. /atom/movable/proc/Hear(atom/movable/speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), message_range=0) SEND_SIGNAL(src, COMSIG_MOVABLE_HEAR, args) - return TRUE + return HEAR_HEARD | HEAR_UNDERSTOOD /** @@ -111,6 +111,24 @@ GLOBAL_LIST_INIT(freqtospan, list( SHOULD_BE_PURE(TRUE) return !HAS_TRAIT(src, TRAIT_MUTE) +/atom/movable/proc/do_tts_message(message, language, message_mods, list/tts_filter, list/hearers) + set waitfor = FALSE + + if(!SStts.tts_enabled || !voice || HAS_TRAIT(src, TRAIT_SIGN_LANG) || HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE) || message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]) + return + + var/list/filter = list() + if(length(voice_filter) > 0) + filter += voice_filter + + if(length(tts_filter) > 0) + filter += tts_filter.Join(",") + var/list/special_filter = list() + INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), hearers, message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) + +/atom/movable/proc/get_tts_voice(list/filter, list/special_filter) + . = voice + /atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language, list/message_mods = list(), forced = FALSE, tts_message, list/tts_filter) var/list/listeners = get_hearers_in_view(range, source) var/list/listened = list() @@ -133,12 +151,10 @@ GLOBAL_LIST_INIT(freqtospan, list( if(!hearing_movable)//theoretically this should use as anything because it shouldnt be able to get nulls but there are reports that it does. stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!") continue - if(hearing_movable.Hear(src, message_language, message, null, null, null, spans, message_mods, range)) + if(hearing_movable.Hear(src, message_language, message, null, null, null, spans, message_mods, range) & HEAR_HEARD) listened += hearing_movable - if(voice) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), listened, message_range = range, pitch = pitch, blip_base = blip_base, blip_number = blip_number, identifier = identifier) - + do_tts_message(tts_message_to_use, message_language, message_mods, tts_filter, listened) /atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), visible_name = FALSE) //This proc uses [] because it is faster than continually appending strings. Thanks BYOND. //Basic span diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm index 6f1423dd9afe..a406cd14a68e 100644 --- a/code/game/sound/sound.dm +++ b/code/game/sound/sound.dm @@ -69,7 +69,10 @@ listeners += listening_ghost for(var/mob/listening_mob in listeners)//had nulls sneak in here, hence the typecheck - if(get_dist_euclidean(listening_mob, turf_source) <= maxdistance) + var/turf/mob_turf = get_turf(listening_mob) + if(!mob_turf) + continue + if(get_dist_euclidean(mob_turf, turf_source) <= maxdistance) listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb, volume_preference) return listeners @@ -216,3 +219,12 @@ return soundin var/datum/sound_effect/sfx = GLOB.sfx_datum_by_key[soundin] return sfx?.return_sfx() || soundin + + +/** + * Creates a soundtoken datum (a sound that updates for movement). + * allowed_listeners is an optional list of mobs that are the only ones that can hear this sound ever. + * sound_length is an optional length of the sound. Things like TTS need to pass this since we can't dynamically grab the length in that case. + */ +/proc/playsoundtoken(atom/source, soundin, volume, range, falloff_exponent = SOUND_FALLOFF_EXPONENT, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, allowed_listeners, sound_length) + return new /datum/sound_token(source, soundin, range, volume, falloff_exponent, falloff_distance, allowed_listeners, sound_length, _delete_on_end = TRUE) diff --git a/code/game/turfs/open/openspace.dm b/code/game/turfs/open/openspace.dm index 68c60752d4e0..380d7bbf3155 100644 --- a/code/game/turfs/open/openspace.dm +++ b/code/game/turfs/open/openspace.dm @@ -149,7 +149,7 @@ return FALSE /turf/open/openspace/CanAStarPass(to_dir, datum/can_pass_info/pass_info) - var/atom/movable/our_movable = pass_info.requester_ref.resolve() + var/atom/movable/our_movable = pass_info.requester_ref?.resolve() if(our_movable && !our_movable.can_z_move(DOWN, src, null, ZMOVE_FALL_FLAGS)) //If we can't fall here (flying/lattice), it's fine to path through return TRUE return FALSE diff --git a/code/modules/admin/verbs/pray.dm b/code/modules/admin/verbs/pray.dm index aefd988aa74d..c0955dec70bb 100644 --- a/code/modules/admin/verbs/pray.dm +++ b/code/modules/admin/verbs/pray.dm @@ -16,31 +16,33 @@ if(src.client.handle_spam_prevention(message, MUTE_PRAY)) return - var/mutable_appearance/cross = mutable_appearance('icons/obj/storage/book.dmi', "bible") - var/font_color = "purple" - var/prayer_type = "PRAYER" - var/deity + + var/prayer_type = DEFAULT_PRAYER + var/list/deities = list() if(src.job == JOB_CHAPLAIN) - cross.icon_state = "kingyellow" - font_color = "blue" - prayer_type = "CHAPLAIN PRAYER" + prayer_type = CHAPLAIN_PRAYER if(GLOB.deity) - deity = GLOB.deity + deities += GLOB.deity else if(IS_CULTIST(src)) - cross.icon_state = "tome" - font_color = "red" - prayer_type = "CULTIST PRAYER" - deity = "Nar'Sie" - else if(isliving(src)) - var/mob/living/L = src - if(HAS_TRAIT(L, TRAIT_SPIRITUAL)) - cross.icon_state = "holylight" - font_color = "blue" - prayer_type = "SPIRITUAL PRAYER" + prayer_type = CULT_PRAYER + deities += "Nar'Sie" + else if(IS_HERETIC_OR_MONSTER(src)) + prayer_type = HERETIC_PRAYER + deities += "the Mansus" + else if(HAS_TRAIT(src, TRAIT_SPIRITUAL)) + prayer_type = SPIRITUAL_PRAYER + else if(HAS_TRAIT(src, TRAIT_EVIL)) + prayer_type = EVIL_PRAYER + + var/mutable_appearance/cross = mutable_appearance('icons/obj/storage/book.dmi', GLOB.prayer_type_to_icon_state[prayer_type]) + + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_SEND_PRAYER, src, message, prayer_type, cross, deities) + var/msg_tmp = message GLOB.requests.pray(src.client, message, src.job == JOB_CHAPLAIN) - message = span_adminnotice("[icon2html(cross, GLOB.admins)][prayer_type][deity ? " (to [deity])" : ""]: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]: [span_linkify(message)]") + message = span_adminnotice("[icon2html(cross, GLOB.admins)][prayer_type][length(deities) ? " (to [english_list(deities)])" : ""]: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]: [span_linkify(message)]") + message = custom_boxed_message(GLOB.prayer_type_to_message_box[prayer_type], message) for(var/client/C in GLOB.admins) if(get_chat_toggles(C) & CHAT_PRAYER) to_chat(C, message, type = MESSAGE_TYPE_PRAYER, confidential = TRUE) diff --git a/code/modules/antagonists/malf_ai/malf_ai.dm b/code/modules/antagonists/malf_ai/malf_ai.dm index 83b84c2e7b91..763ab99c4ce9 100644 --- a/code/modules/antagonists/malf_ai/malf_ai.dm +++ b/code/modules/antagonists/malf_ai/malf_ai.dm @@ -118,8 +118,8 @@ if(istype(datum_owner)) datum_owner.hack_software = TRUE - datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_phrase_regex, "blue", src) - datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_response_regex, "red", src) + datum_owner.AddComponent(/datum/component/codeword_hearing, SStraitor.syndicate_code_phrase_regex, "blue", src) + datum_owner.AddComponent(/datum/component/codeword_hearing, SStraitor.syndicate_code_response_regex, "red", src) /datum/antagonist/malf_ai/remove_innate_effects(mob/living/mob_override) var/mob/living/silicon/ai/datum_owner = mob_override || owner.current @@ -135,8 +135,8 @@ if(!owner.current) return - var/phrases = jointext(GLOB.syndicate_code_phrase, ", ") - var/responses = jointext(GLOB.syndicate_code_response, ", ") + var/phrases = jointext(SStraitor.syndicate_code_phrase, ", ") + var/responses = jointext(SStraitor.syndicate_code_response, ", ") antag_memory += "Code Phrase: [span_blue("[phrases]")]
" antag_memory += "Code Response: [span_red("[responses]")]
" @@ -176,8 +176,8 @@ data["has_codewords"] = should_give_codewords if(should_give_codewords) - data["phrases"] = jointext(GLOB.syndicate_code_phrase, ", ") - data["responses"] = jointext(GLOB.syndicate_code_response, ", ") + data["phrases"] = jointext(SStraitor.syndicate_code_phrase, ", ") + data["responses"] = jointext(SStraitor.syndicate_code_response, ", ") data["intro"] = malfunction_flavor["introduction"] data["allies"] = malfunction_flavor["allies"] data["goal"] = malfunction_flavor["goal"] diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm index 0fdb4251f5f4..5f810a1e37cb 100644 --- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm +++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm @@ -1097,7 +1097,7 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf)) /obj/structure, /obj/item/radio/intercom, /obj/item/modular_computer, - /mob/living/simple_animal/bot, + /mob/living/basic/bot, /mob/living/silicon, ) diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm index b3f37afae865..3d457a67c28c 100644 --- a/code/modules/antagonists/revolution/revolution.dm +++ b/code/modules/antagonists/revolution/revolution.dm @@ -7,8 +7,9 @@ antag_hud_name = "rev" suicide_cry = "VIVA LA REVOLUTION!!" stinger_sound = 'sound/music/antag/revolutionary_tide.ogg' - var/datum/team/revolution/rev_team + ui_name = "AntagInfoRevolution" + var/datum/team/revolution/rev_team /// When this antagonist is being de-antagged, this is the source. Can be a mob (for mindshield/blunt force trauma) or a #define string. var/deconversion_source @@ -20,18 +21,6 @@ return ..() /datum/antagonist/rev/admin_add(datum/mind/new_owner, mob/admin) - // No revolution exists which means admin adding this will create a new revolution team - // This causes problems because revolution teams (currently) require a dynamic datum to process its victory / defeat conditions - if(!(locate(/datum/team/revolution) in GLOB.antagonist_teams)) - var/confirm = tgui_alert(admin, "Notice: Revolutions do not function 100% when created via traitor panel instead of dynamic. \ - The leaders will be able to convert as normal, but the shuttle will not be blocked and there will be no announcements when either side wins. \ - Are you sure?", "Be Wary", list("Yes", "No")) - if(QDELETED(src) || QDELETED(new_owner.current) || confirm != "Yes") - return - - go_through_with_admin_add(new_owner, admin) - -/datum/antagonist/rev/proc/go_through_with_admin_add(datum/mind/new_owner, mob/admin) new_owner.add_antag_datum(src) message_admins("[key_name_admin(admin)] has rev'ed [key_name_admin(new_owner)].") log_admin("[key_name(admin)] has rev'ed [key_name(new_owner)].") @@ -58,11 +47,6 @@ equip_rev() owner.current.log_message("has been converted to the revolution!", LOG_ATTACK, color="red") -/datum/antagonist/rev/greet() - . = ..() - to_chat(owner, span_userdanger("Help your cause. Do not harm your fellow freedom fighters. You can identify your comrades by the red \"R\" icons, and your leaders by the blue \"R\" icons. Help them kill the heads to win the revolution!")) - owner.announce_objectives() - /datum/antagonist/rev/create_team(datum/team/revolution/new_team) if(!new_team) GLOB.revolution_handler ||= new() @@ -98,7 +82,20 @@ message_admins("[key_name_admin(admin)] has head-rev'ed [O].") log_admin("[key_name(admin)] has head-rev'ed [O].") -/datum/antagonist/rev/head/go_through_with_admin_add(datum/mind/new_owner, mob/admin) +/datum/antagonist/rev/ui_static_data(mob/user) + . = ..() + .["leader"] = (pref_flag == ROLE_REV_HEAD) + .["heads"] = list() + for(var/datum/mind/head_of_staff as anything in SSjob.get_all_heads()) + .["heads"] += list(list("name" = head_of_staff.name, "role" = head_of_staff.assigned_role.title)) + +/datum/antagonist/rev/head/ui_static_data(mob/user) + . = ..() + .["code_phrases"] = rev_team.head_chose_phrase_raw + .["code_responses"] = rev_team.head_code_responses_raw + .["lone_wolf"] = !roundstart || length(rev_team.get_head_revolutionaries()) == 1 + +/datum/antagonist/rev/head/admin_add(datum/mind/new_owner, mob/admin) give_flash = TRUE give_hud = TRUE remove_clumsy = TRUE @@ -161,6 +158,7 @@ var/remove_clumsy = FALSE var/give_flash = FALSE var/give_hud = TRUE + var/roundstart = FALSE /datum/antagonist/rev/head/pre_mindshield(mob/implanter, mob/living/mob_override) return COMPONENT_MINDSHIELD_RESISTED @@ -182,12 +180,18 @@ real_mob.AddComponentFrom(REF(src), /datum/component/can_flash_from_behind) RegisterSignal(real_mob, COMSIG_MOB_SUCCESSFUL_FLASHED_MOB, PROC_REF(on_flash_success)) + real_mob.AddComponent(/datum/component/codeword_hearing, rev_team.head_code_phrases, "blue", src) + real_mob.AddComponent(/datum/component/codeword_hearing, rev_team.head_code_responses, "red", src) + /datum/antagonist/rev/head/remove_innate_effects(mob/living/mob_override) . = ..() var/mob/living/real_mob = mob_override || owner.current real_mob.RemoveComponentSource(REF(src), /datum/component/can_flash_from_behind) UnregisterSignal(real_mob, COMSIG_MOB_SUCCESSFUL_FLASHED_MOB) + for(var/datum/component/codeword_hearing/component as anything in real_mob.GetComponents(/datum/component/codeword_hearing)) + component.delete_if_from_source(src) + /// Signal proc for [COMSIG_MOB_SUCCESSFUL_FLASHED_MOB]. /// Bread and butter of revolution conversion, successfully flashing a carbon will make them a revolutionary /datum/antagonist/rev/head/proc/on_flash_success(mob/living/source, mob/living/flashed, obj/item/assembly/flash/flash, deviation) @@ -371,6 +375,23 @@ /// List of all ex-revs. Useful because dynamic removes antag status when it ends, so this can be kept for the roundend report. var/list/datum/mind/ex_revs = list() + /// List of code phrases + VAR_FINAL/list/head_chose_phrase_raw + /// Regex for code phrases that only headrevs hear + VAR_FINAL/regex/head_code_phrases + /// List of code responses + VAR_FINAL/list/head_code_responses_raw + /// Regex for code responses that only headrevs hear + VAR_FINAL/regex/head_code_responses + +/datum/team/revolution/New(starting_members) + . = ..() + head_chose_phrase_raw = generate_code_phrase(return_list = TRUE) + head_code_phrases = new("([jointext(head_chose_phrase_raw, "|")])", "ig") + + head_chose_phrase_raw = generate_code_phrase(return_list = TRUE) + head_code_responses = new("([jointext(head_chose_phrase_raw, "|")])", "ig") + /// Saves all current headrevs and revs /datum/team/revolution/proc/save_members() ex_headrevs = get_head_revolutionaries() diff --git a/code/modules/antagonists/revolution/revolution_handler.dm b/code/modules/antagonists/revolution/revolution_handler.dm index b429500d7c88..3ffc270f4643 100644 --- a/code/modules/antagonists/revolution/revolution_handler.dm +++ b/code/modules/antagonists/revolution/revolution_handler.dm @@ -115,7 +115,7 @@ GLOBAL_DATUM(revolution_handler, /datum/revolution_handler) /datum/revolution_handler/proc/check_rev_victory() for(var/datum/objective/mutiny/objective in revs.objectives) - if(!(objective.check_completion())) + if(!objective.check_completion()) return FALSE return TRUE diff --git a/code/modules/antagonists/santa/santa.dm b/code/modules/antagonists/santa/santa.dm index f3e1cbd02b96..8f6765604e03 100644 --- a/code/modules/antagonists/santa/santa.dm +++ b/code/modules/antagonists/santa/santa.dm @@ -4,6 +4,7 @@ show_name_in_check_antagonists = TRUE show_to_ghosts = TRUE suicide_cry = "FOR CHRISTMAS!!" + var/datum/component/listen_prayers/santa_prayers /datum/antagonist/santa/on_gain() . = ..() @@ -12,6 +13,30 @@ owner.add_traits(list(TRAIT_CANNOT_OPEN_PRESENTS, TRAIT_PRESENT_VISION), TRAIT_SANTA) + santa_prayers = owner.AddComponent(/datum/component/listen_prayers, CALLBACK(src, PROC_REF(check_if_santa_prayer)), "Santa Claus", "Allows you to listen for prayers that mention you or Christmas.") + +/datum/antagonist/santa/proc/check_if_santa_prayer(list/arguments) + SIGNAL_HANDLER + var/message = arguments[ARG_PRAYER_MSG] + var/mob/boy_girl = arguments[ARG_PRAYING_MOB] + var/regex/santa_regex = regex("(santa|claus|christmas|xmas)", "i") + if(!santa_regex.Find(message) && (!prob(60) || !findtext(message, "satan"))) + return FALSE //The message doesn't mention us (or satan, cuz the names are so similar, accidents may happen) + var/is_good_boy_girl = !boy_girl.is_antag() && !HAS_TRAIT(boy_girl, TRAIT_EVIL) + arguments[ARG_PRAYER_TYPE] = is_good_boy_girl ? SANTA_PRAYER : SANTA_NAUGHTY_PRAYER + arguments[ARG_PRAYER_SYMBOL] = icon('icons/obj/storage/wrapping.dmi', "giftdeliverypackage4") + return TRUE + +/datum/antagonist/santa/on_removal() + if(!owner) + return ..() + owner.remove_traits(list(TRAIT_CANNOT_OPEN_PRESENTS, TRAIT_PRESENT_VISION), TRAIT_SANTA) + QDEL_NULL(santa_prayers) + if(owner.current) + var/datum/action/cooldown/spell/teleport/area_teleport/wizard/santa/teleport = locate() in owner.current.actions + qdel(teleport) + return ..() + /datum/antagonist/santa/greet() . = ..() to_chat(owner, span_bolddanger("Your objective is to bring joy to the people on this station. You have a magical bag, which generates presents as long as you have it! You can examine the presents to take a peek inside, to make sure that you give the right gift to the right person.")) diff --git a/code/modules/antagonists/spy/spy_bounty.dm b/code/modules/antagonists/spy/spy_bounty.dm index 5344f86be2ea..5c65e544dbc5 100644 --- a/code/modules/antagonists/spy/spy_bounty.dm +++ b/code/modules/antagonists/spy/spy_bounty.dm @@ -679,7 +679,7 @@ /datum/spy_bounty/some_bot/get_dupe_protection_key(atom/movable/stealing) return bot_type -/datum/spy_bounty/some_bot/finish_cleanup(mob/living/simple_animal/bot/stealing) +/datum/spy_bounty/some_bot/finish_cleanup(mob/living/basic/bot/stealing) if(stealing.client) to_chat(stealing, span_deadsay("You've been stolen! You are shipped off to the black market and taken apart for spare parts...")) stealing.investigate_log("stole by a spy (and deleted)", INVESTIGATE_DEATHS) diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index bafdd65d06bb..0fa2bf16f4e2 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -312,8 +312,8 @@ handle_clown_mutation(datum_owner, mob_override ? null : "Your training has allowed you to overcome your clownish nature, allowing you to wield weapons without harming yourself.") if(should_give_codewords) - datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_phrase_regex, "blue", src) - datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_response_regex, "red", src) + datum_owner.AddComponent(/datum/component/codeword_hearing, SStraitor.syndicate_code_phrase_regex, "blue", src) + datum_owner.AddComponent(/datum/component/codeword_hearing, SStraitor.syndicate_code_response_regex, "red", src) /datum/antagonist/traitor/remove_innate_effects(mob/living/mob_override) var/mob/living/datum_owner = mob_override || owner.current @@ -333,8 +333,8 @@ var/list/data = list() data["has_codewords"] = should_give_codewords if(should_give_codewords) - data["phrases"] = jointext(GLOB.syndicate_code_phrase, ", ") - data["responses"] = jointext(GLOB.syndicate_code_response, ", ") + data["phrases"] = jointext(SStraitor.syndicate_code_phrase, ", ") + data["responses"] = jointext(SStraitor.syndicate_code_response, ", ") data["theme"] = traitor_flavor["ui_theme"] data["code"] = uplink?.unlock_code data["failsafe_code"] = uplink?.failsafe_code @@ -431,8 +431,8 @@ return sent_data /datum/antagonist/traitor/roundend_report_footer() - var/phrases = jointext(GLOB.syndicate_code_phrase, ", ") - var/responses = jointext(GLOB.syndicate_code_response, ", ") + var/phrases = jointext(SStraitor.syndicate_code_phrase, ", ") + var/responses = jointext(SStraitor.syndicate_code_response, ", ") var/message = "
The code phrases were: [phrases]
\ The code responses were: [span_redtext("[responses]")]
" diff --git a/code/modules/assembly/infrared.dm b/code/modules/assembly/infrared.dm index e48ae62b576a..f67e15ab0362 100644 --- a/code/modules/assembly/infrared.dm +++ b/code/modules/assembly/infrared.dm @@ -75,7 +75,9 @@ return FALSE /// Used to refresh the beam in whatever context. -/obj/item/assembly/infra/proc/make_beam() +/// glide_seed/glide_time (from a predecessor beam's get_last_geometry) make the rebuilt beam glide +/// from the old position instead of snapping; left as defaults for non-movement refreshes. +/obj/item/assembly/infra/proc/make_beam(list/glide_seed = null, glide_time = 0) SHOULD_NOT_SLEEP(TRUE) if(!isnull(buffer_turf)) @@ -115,6 +117,8 @@ emissive = TRUE, override_target_pixel_x = pixel_x, override_target_pixel_y = pixel_y, + glide_seed = glide_seed, + glide_time = glide_time, ) RegisterSignal(active_beam, COMSIG_BEAM_ENTERED, PROC_REF(beam_entered)) RegisterSignal(active_beam, COMSIG_BEAM_TURFS_CHANGED, PROC_REF(beam_turfs_changed)) @@ -244,30 +248,15 @@ make_beam() /obj/item/assembly/infra/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + var/list/glide_seed = active_beam?.get_last_geometry() . = ..() if(loc == old_loc) return - make_beam() if(!visible || forced || !movement_dir || !Adjacent(old_loc)) + make_beam() return - // Because the new beam is made in the new loc, it "jumps" from one turf to another - // We can do an animate to pretend we're gliding between turfs rather than making a whole new beam - var/x_move = 0 - var/y_move = 0 - if(movement_dir & NORTH) - y_move = -ICON_SIZE_Y - else if(movement_dir & SOUTH) - y_move = ICON_SIZE_Y - if(movement_dir & WEST) - x_move = ICON_SIZE_X - else if(movement_dir & EAST) - x_move = -ICON_SIZE_X - - var/fake_glide_time = round(ICON_SIZE_ALL / glide_size * world.tick_lag, world.tick_lag) - for(var/obj/effect/ebeam/beam as anything in active_beam?.elements) - var/matrix/base_transform = matrix(beam.transform) - beam.transform = beam.transform.Translate(x_move, y_move) - animate(beam, transform = base_transform, time = fake_glide_time) + var/glide_time = ICON_SIZE_ALL / max(glide_size, MIN_GLIDE_SIZE) * world.tick_lag * GLOB.glide_size_multiplier + make_beam(glide_seed, glide_time) /obj/item/assembly/infra/setDir(newdir) var/prev_dir = dir diff --git a/code/modules/assembly/voice.dm b/code/modules/assembly/voice.dm index ebf9d71a8ddf..11b74f718d4d 100644 --- a/code/modules/assembly/voice.dm +++ b/code/modules/assembly/voice.dm @@ -49,7 +49,7 @@ else if(check_activation(speaker, raw_message)) send_pulse() - return TRUE + return HEAR_HEARD | HEAR_UNDERSTOOD /obj/item/assembly/voice/proc/record_speech(atom/movable/speaker, raw_message, datum/language/message_language) switch(mode) diff --git a/code/modules/cargo/markets/market_items/hostages.dm b/code/modules/cargo/markets/market_items/hostages.dm index 518ba0c9cf35..04bd070e6b01 100644 --- a/code/modules/cargo/markets/market_items/hostages.dm +++ b/code/modules/cargo/markets/market_items/hostages.dm @@ -78,7 +78,7 @@ var/obj/item/clothing/under/misc/syndicate_souvenir/souvenir = new(loc) humie.equip_to_slot_if_possible(souvenir, ITEM_SLOT_ICLOTHING, indirect_action = TRUE) var/obj/item/clothing/accessory/anti_sec_pin/pin = new(loc) - pin.attach(souvenir) + pin.try_attach(souvenir) if(isnull(humie.w_uniform)) //FUCKING SLAVES, GET YOUR CLOTHES BACK ON! diff --git a/code/modules/cargo/packs/_packs.dm b/code/modules/cargo/packs/_packs.dm index 859cb7a99f0b..e6ed881e76d8 100644 --- a/code/modules/cargo/packs/_packs.dm +++ b/code/modules/cargo/packs/_packs.dm @@ -60,7 +60,7 @@ var/obj/structure/closet/crate/C if(paying_account) C = new /obj/structure/closet/crate/secure/owned(A, paying_account) - C.name = "[crate_name] - Purchased by [paying_account.account_holder]" + C.name = "[crate_name || C.name] - Purchased by [paying_account.account_holder]" else if(!crate_type && !crate_override) CRASH("tried to generate a supply pack without a valid crate type") else if(crate_override) @@ -70,7 +70,8 @@ else C = new crate_type(A) - C.name = crate_name + if(crate_name) + C.name = crate_name if(access) C.req_access = list(access) if(access_any) diff --git a/code/modules/cargo/packs/medical.dm b/code/modules/cargo/packs/medical.dm index 93f7fa12ed84..da7088b455df 100644 --- a/code/modules/cargo/packs/medical.dm +++ b/code/modules/cargo/packs/medical.dm @@ -30,7 +30,7 @@ contains = list(/obj/item/reagent_containers/hypospray/medipen = 2, /obj/item/reagent_containers/hypospray/medipen/ekit = 3, /obj/item/reagent_containers/hypospray/medipen/blood_loss = 3) - crate_name = "medipen crate" + crate_name = null crate_type = /obj/structure/closet/crate/deforest /datum/supply_pack/medical/coroner_crate @@ -48,23 +48,24 @@ name = "Chemical Starter Kit Crate" desc = "Contains thirteen different chemicals, for all the fun experiments you can make." cost = CARGO_CRATE_VALUE * 2.6 - contains = list(/obj/item/reagent_containers/cup/bottle/hydrogen, - /obj/item/reagent_containers/cup/bottle/carbon, - /obj/item/reagent_containers/cup/bottle/nitrogen, - /obj/item/reagent_containers/cup/bottle/oxygen, - /obj/item/reagent_containers/cup/bottle/fluorine, - /obj/item/reagent_containers/cup/bottle/phosphorus, - /obj/item/reagent_containers/cup/bottle/silicon, - /obj/item/reagent_containers/cup/bottle/chlorine, - /obj/item/reagent_containers/cup/bottle/radium, - /obj/item/reagent_containers/cup/bottle/sacid, - /obj/item/reagent_containers/cup/bottle/ethanol, - /obj/item/reagent_containers/cup/bottle/potassium, - /obj/item/reagent_containers/cup/bottle/sugar, - /obj/item/clothing/glasses/science, - /obj/item/reagent_containers/dropper, - /obj/item/storage/box/beakers, - ) + contains = list( + /obj/item/reagent_containers/cup/bottle/hydrogen, + /obj/item/reagent_containers/cup/bottle/carbon, + /obj/item/reagent_containers/cup/bottle/nitrogen, + /obj/item/reagent_containers/cup/bottle/oxygen, + /obj/item/reagent_containers/cup/bottle/fluorine, + /obj/item/reagent_containers/cup/bottle/phosphorus, + /obj/item/reagent_containers/cup/bottle/silicon, + /obj/item/reagent_containers/cup/bottle/chlorine, + /obj/item/reagent_containers/cup/bottle/radium, + /obj/item/reagent_containers/cup/bottle/sacid, + /obj/item/reagent_containers/cup/bottle/ethanol, + /obj/item/reagent_containers/cup/bottle/potassium, + /obj/item/reagent_containers/cup/bottle/sugar, + /obj/item/clothing/glasses/science, + /obj/item/reagent_containers/dropper, + /obj/item/storage/box/beakers, + ) crate_name = "chemical crate" /datum/supply_pack/medical/defibs @@ -72,48 +73,49 @@ desc = "Contains two defibrillators for bringing the recently deceased back to life." cost = CARGO_CRATE_VALUE * 5 contains = list(/obj/item/defibrillator/loaded = 2) - crate_name = "defibrillator crate" - crate_type = /obj/structure/closet/crate/medical + crate_name = null + crate_type = /obj/structure/closet/crate/deforest /datum/supply_pack/medical/iv_drip name = "IV Drip Crate" desc = "Contains a single IV drip for administering blood to patients." cost = CARGO_CRATE_VALUE * 2 contains = list(/obj/machinery/iv_drip) - crate_name = "iv drip crate" - crate_type = /obj/structure/closet/crate/medical + crate_name = null + crate_type = /obj/structure/closet/crate/deforest /datum/supply_pack/medical/supplies name = "Medical Supplies Crate" desc = "Contains a random assortment of medical supplies. German doctor not included." cost = CARGO_CRATE_VALUE * 4 - contains = list(/obj/item/reagent_containers/cup/bottle/multiver, - /obj/item/reagent_containers/cup/bottle/epinephrine, - /obj/item/reagent_containers/cup/bottle/morphine, - /obj/item/reagent_containers/cup/bottle/toxin, - /obj/item/reagent_containers/cup/beaker/large, - /obj/item/reagent_containers/applicator/pill/insulin, - /obj/item/stack/medical/wrap/gauze, - /obj/item/storage/box/bandages, - /obj/item/storage/box/beakers, - /obj/item/storage/box/medigels, - /obj/item/storage/box/syringes, - /obj/item/storage/box/bodybags, - /obj/item/storage/medkit/regular, - /obj/item/storage/medkit/o2, - /obj/item/storage/medkit/toxin, - /obj/item/storage/medkit/brute, - /obj/item/storage/medkit/fire, - /obj/item/defibrillator/loaded, - /obj/item/reagent_containers/blood/o_minus, - /obj/item/storage/pill_bottle/mining, - /obj/item/reagent_containers/applicator/pill/neurine, - /obj/item/stack/medical/bone_gel = 2, - /obj/item/vending_refill/medical, - /obj/item/vending_refill/drugs, - ) - crate_name = "medical supplies crate" - crate_type = /obj/structure/closet/crate/medical + contains = list( + /obj/item/reagent_containers/cup/bottle/multiver, + /obj/item/reagent_containers/cup/bottle/epinephrine, + /obj/item/reagent_containers/cup/bottle/morphine, + /obj/item/reagent_containers/cup/bottle/toxin, + /obj/item/reagent_containers/cup/beaker/large, + /obj/item/reagent_containers/applicator/pill/insulin, + /obj/item/stack/medical/wrap/gauze, + /obj/item/storage/box/bandages, + /obj/item/storage/box/beakers, + /obj/item/storage/box/medigels, + /obj/item/storage/box/syringes, + /obj/item/storage/box/bodybags, + /obj/item/storage/medkit/regular, + /obj/item/storage/medkit/o2, + /obj/item/storage/medkit/toxin, + /obj/item/storage/medkit/brute, + /obj/item/storage/medkit/fire, + /obj/item/defibrillator/loaded, + /obj/item/reagent_containers/blood/o_minus, + /obj/item/storage/pill_bottle/mining, + /obj/item/reagent_containers/applicator/pill/neurine, + /obj/item/stack/medical/bone_gel = 2, + /obj/item/vending_refill/medical, + /obj/item/vending_refill/drugs, + ) + crate_name = null + crate_type = /obj/structure/closet/crate/deforest test_ignored = TRUE /datum/supply_pack/medical/supplies/fill(obj/container) @@ -126,8 +128,8 @@ desc = "A crate containing the medication required for living with Hereditary Manifold Sickness, Sansufentanyl." cost = CARGO_CRATE_VALUE * 3 contains = list(/obj/item/storage/pill_bottle/sansufentanyl = 2) - crate_name = "experimental medicine crate" - crate_type = /obj/structure/closet/crate/medical + crate_name = null + crate_type = /obj/structure/closet/crate/interdyne_normal /datum/supply_pack/medical/surgery name = "Surgical Supplies Crate" @@ -140,7 +142,7 @@ /obj/item/reagent_containers/medigel/sterilizine, /obj/item/emergency_bed, ) - crate_name = "surgical supplies crate" + crate_name = null crate_type = /obj/structure/closet/crate/deforest /datum/supply_pack/medical/salglucanister @@ -161,19 +163,20 @@ cost = CARGO_CRATE_VALUE * 5 access = ACCESS_CMO access_view = ACCESS_VIROLOGY - contains = list(/obj/item/reagent_containers/cup/bottle/flu_virion, - /obj/item/reagent_containers/cup/bottle/cold, - /obj/item/reagent_containers/cup/bottle/random_virus = 4, - /obj/item/reagent_containers/cup/bottle/fake_gbs, - /obj/item/reagent_containers/cup/bottle/magnitis, - /obj/item/reagent_containers/cup/bottle/pierrot_throat, - /obj/item/reagent_containers/cup/bottle/brainrot, - /obj/item/reagent_containers/cup/bottle/anxiety, - /obj/item/reagent_containers/cup/bottle/beesease, - /obj/item/storage/box/syringes, - /obj/item/storage/box/beakers, - /obj/item/reagent_containers/cup/bottle/mutagen, - ) + contains = list( + /obj/item/reagent_containers/cup/bottle/flu_virion, + /obj/item/reagent_containers/cup/bottle/cold, + /obj/item/reagent_containers/cup/bottle/random_virus = 4, + /obj/item/reagent_containers/cup/bottle/fake_gbs, + /obj/item/reagent_containers/cup/bottle/magnitis, + /obj/item/reagent_containers/cup/bottle/pierrot_throat, + /obj/item/reagent_containers/cup/bottle/brainrot, + /obj/item/reagent_containers/cup/bottle/anxiety, + /obj/item/reagent_containers/cup/bottle/beesease, + /obj/item/storage/box/syringes, + /obj/item/storage/box/beakers, + /obj/item/reagent_containers/cup/bottle/mutagen, + ) crate_name = "virus crate" crate_type = /obj/structure/closet/crate/secure/plasma order_flags = ORDER_DANGEROUS @@ -183,16 +186,17 @@ desc = "Contains the CMO's turtleneck and turtleneck skirt." cost = CARGO_CRATE_VALUE * 2 access = ACCESS_CMO - contains = list(/obj/item/clothing/under/rank/medical/chief_medical_officer/turtleneck, - /obj/item/clothing/under/rank/medical/chief_medical_officer/turtleneck/skirt, - ) + contains = list( + /obj/item/clothing/under/rank/medical/chief_medical_officer/turtleneck, + /obj/item/clothing/under/rank/medical/chief_medical_officer/turtleneck/skirt, + ) /datum/supply_pack/medical/arm_implants name = "Strong-Arm Implant Set" desc = "A crate containing two implants, which can be surgically implanted to empower the strength of human arms. Warranty void if exposed to electromagnetic pulses." cost = CARGO_CRATE_VALUE * 6 contains = list(/obj/item/organ/cyberimp/arm/strongarm = 2) - crate_name = "Strong-Arm implant crate" + crate_name = "\improper Strong-Arm implant crate" discountable = SUPPLY_PACK_RARE_DISCOUNTABLE /datum/supply_pack/medical/paperwork_implants @@ -244,4 +248,14 @@ /obj/item/sensor_device, /obj/item/sensor_device, ) - crate_name = "handheld crew monitor crate" + crate_name = null + crate_type = /obj/structure/closet/crate/deforest + +/datum/supply_pack/medical/first_aid_board + name = "DeForest First Aid Station" + desc = "A crate containing a circuit board used to construct a DeForest First Aid Station - \ + a machine designed to treat minor injuries and ailments. Components not included." + cost = CARGO_CRATE_VALUE * 6 + contains = list(/obj/item/circuitboard/machine/wall_healer) + crate_name = null + crate_type = /obj/structure/closet/crate/deforest diff --git a/code/modules/cargo/packs/service.dm b/code/modules/cargo/packs/service.dm index 4092fcfe25d4..3a97e99ee54d 100644 --- a/code/modules/cargo/packs/service.dm +++ b/code/modules/cargo/packs/service.dm @@ -68,7 +68,7 @@ desc = "Pink-haired Quartermaster not doing her job? Replace her with this tireless worker, today! \ Contains one MULEbot." cost = CARGO_CRATE_VALUE * 4 - contains = list(/mob/living/simple_animal/bot/mulebot) + contains = list(/mob/living/basic/bot/mulebot) crate_name = "\improper MULEbot Crate" crate_type = /obj/structure/closet/crate/large diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index ff0077c7251d..20693fc1bed5 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -632,6 +632,8 @@ GLOBAL_LIST_INIT(unrecommended_builds, list( QDEL_LIST_ASSOC_VAL(char_render_holders) + sound_tokens = null + SSambience.remove_ambience_client(src) SSmouse_entered.hovers -= src SSping.currentrun -= src @@ -643,6 +645,7 @@ GLOBAL_LIST_INIT(unrecommended_builds, list( QDEL_NULL(loot_panel) QDEL_NULL(parallax_rock) seen_messages = null + sound_tokens = null Master.UpdateTickRate() ..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening return QDEL_HINT_HARDDEL_NOW diff --git a/code/modules/client/preferences/middleware/tts.dm b/code/modules/client/preferences/middleware/tts.dm index 311f9647d613..cedd099db620 100644 --- a/code/modules/client/preferences/middleware/tts.dm +++ b/code/modules/client/preferences/middleware/tts.dm @@ -5,7 +5,7 @@ action_delegations = list( "play_voice" = PROC_REF(play_voice), - "play_voice_borg" = PROC_REF(play_voice_borg), + "play_voice_robot" = PROC_REF(play_voice_robot), "play_blips" = PROC_REF(play_blips), ) @@ -24,7 +24,7 @@ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Привет, это мой голос.", speaker = speaker, pitch = pitch, local = TRUE, blip_base = blip_base, blip_number = blip_number) return TRUE -/datum/preference_middleware/tts/proc/play_voice_borg(list/params, mob/user) +/datum/preference_middleware/tts/proc/play_voice_robot(list/params, mob/user) if(!COOLDOWN_FINISHED(src, tts_test_cooldown)) return TRUE var/speaker = preferences.read_preference(/datum/preference/choiced/voice) diff --git a/code/modules/client/preferences/particle_weather.dm b/code/modules/client/preferences/particle_weather.dm new file mode 100644 index 000000000000..a9185eb5d13c --- /dev/null +++ b/code/modules/client/preferences/particle_weather.dm @@ -0,0 +1,15 @@ +/// Whether or not to toggle ambient occlusion, the shadows around people +/datum/preference/toggle/particle_weather + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "particle_weather" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/particle_weather/apply_to_client(client/client, value) + for(var/atom/movable/screen/plane_master/rendering_plate/particle_weather/plane_master as anything in client.mob?.hud_used?.get_true_plane_masters(RENDER_PLANE_PARTICLE_WEATHER)) + plane_master.update_state(client.mob) + + for(var/atom/movable/screen/plane_master/weather/plane_master as anything in client.mob?.hud_used?.get_true_plane_masters(WEATHER_PLANE)) + plane_master.update_state(client.mob) + + for(var/atom/movable/screen/plane_master/weather/plane_master as anything in client.mob?.hud_used?.get_true_plane_masters(PARTICLE_WEATHER_PLANE)) + plane_master.update_state(client.mob) diff --git a/code/modules/clothing/chameleon/generic_chameleon_clothing.dm b/code/modules/clothing/chameleon/generic_chameleon_clothing.dm index aff76cc915f4..a7fac34a2ab3 100644 --- a/code/modules/clothing/chameleon/generic_chameleon_clothing.dm +++ b/code/modules/clothing/chameleon/generic_chameleon_clothing.dm @@ -192,6 +192,11 @@ do { \ action_slots = ALL clothing_traits = list(TRAIT_VOICE_MATCHES_ID) +/obj/item/clothing/mask/chameleon/proc/after_input_check(mob/user) + if(QDELETED(user) || QDELETED(src) || !user.client || !user.can_perform_action(src, NEED_DEXTERITY|FORBID_TELEKINESIS_REACH)) + return FALSE + return TRUE + /obj/item/clothing/mask/chameleon/attack_self(mob/user) var/on = (TRAIT_VOICE_MATCHES_ID in clothing_traits) if(on) @@ -199,14 +204,39 @@ do { \ detach_clothing_traits(TRAIT_VOICE_MATCHES_ID) else if(SStts.tts_enabled) - // MASSMETA EDIT START (ntts && /tg/tts) ORIGINAL: var/voice_choice = tgui_input_list(user, "Choose what voice to use as a disguise", "Voice Selection", SStts.available_speakers) - var/voice_choice = tgui_input_list(user, "Choose what voice to use as a disguise", "Voice Selection", SStts.player_voice_choices()) - // MASSMETA EDIT END (ntts && /tg/tts) - if(isnull(voice_choice)) - to_chat(user, span_warning("No choice selected, audible voice changing disabled.")) - voice_override = null + var/popup_input = tgui_input_list(user, "Choose Action", "Chameleon Mask", list("Spoof Crew Manifest Voice", "Spoof Any Voice", "Cancel")) + if(!popup_input || !after_input_check(user)) return - voice_override = voice_choice + switch(popup_input) + if ("Spoof Crew Manifest Voice") + var/list/possible_voices = list() + for(var/datum/record/crew/target in GLOB.manifest.general) + if(target.voice && (target.voice in SStts.player_voice_choices())) + possible_voices += target.name + CHECK_TICK + // MASSMETA EDIT START (ntts && /tg/tts) + // ORIGINAL: var/voice_choice = tgui_input_list(user, "Choose what voice to use as a disguise", "Voice Selection") + var/voice_choice = tgui_input_list(user, "Choose what voice to use as a disguise", "Voice Selection", SStts.player_voice_choices()) + // MASSMETA EDIT END (ntts /tg/tts) + if(isnull(voice_choice) || !after_input_check(user)) + to_chat(user, span_warning("No choice selected, audible voice changing disabled.")) + voice_override = null + return + var/datum/record/crew/crew_record = find_record(voice_choice) + if(crew_record.voice && (crew_record.voice in SStts.player_voice_choices())) + voice_override = voice_choice + return + else + to_chat(user, span_warning("Crewmember's record's voice has been changed, please select another.")) + return + if("Spoof Any Voice") + var/voice_choice = tgui_input_list(user, "Choose what voice to use as a disguise", "Voice Selection", SStts.player_voice_choices()) + if(isnull(voice_choice) || !after_input_check(user)) + to_chat(user, span_warning("No choice selected, audible voice changing disabled.")) + voice_override = null + return + voice_override = voice_choice + else voice_override = null attach_clothing_traits(TRAIT_VOICE_MATCHES_ID) diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm index 730cb03dae66..3dd8c07ab183 100644 --- a/code/modules/clothing/glasses/_glasses.dm +++ b/code/modules/clothing/glasses/_glasses.dm @@ -592,16 +592,7 @@ color_cutoffs = null vision_flags = SEE_TURFS|SEE_MOBS|SEE_OBJS glass_colour_type = /datum/client_colour/glass_colour/lightblue - -/obj/item/clothing/glasses/thermal/xray/equipped(mob/living/carbon/human/user, slot) - . = ..() - if(!(slot & ITEM_SLOT_EYES) || !istype(user)) - return - ADD_TRAIT(user, TRAIT_XRAY_VISION, GLASSES_TRAIT) - -/obj/item/clothing/glasses/thermal/xray/dropped(mob/living/carbon/human/user) - . = ..() - REMOVE_TRAIT(user, TRAIT_XRAY_VISION, GLASSES_TRAIT) + clothing_traits = list(TRAIT_XRAY_VISION) /obj/item/clothing/glasses/thermal/syndi name = "chameleon thermals" diff --git a/code/modules/clothing/head/frenchberet.dm b/code/modules/clothing/head/frenchberet.dm index 90c2e8300944..bdf8fcb2a694 100644 --- a/code/modules/clothing/head/frenchberet.dm +++ b/code/modules/clothing/head/frenchberet.dm @@ -2,18 +2,8 @@ name = "french beret" desc = "A quality beret, infused with the aroma of chain-smoking, wine-swilling Parisians. You feel less inclined to engage in military conflict, for some reason." flags_1 = NO_NEW_GAGS_PREVIEW_1 + clothing_traits = list(TRAIT_GARLIC_BREATH) /obj/item/clothing/head/beret/frenchberet/Initialize(mapload) . = ..() AddComponent(/datum/component/speechmod, replacements = strings("french_replacement.json", "french"), end_string = list(" Honh honh honh!"," Honh!"," Zut Alors!"), end_string_chance = 3, slots = ITEM_SLOT_HEAD) - -/obj/item/clothing/head/beret/frenchberet/equipped(mob/user, slot, initial) - . = ..() - if (slot & ITEM_SLOT_HEAD) - ADD_TRAIT(user, TRAIT_GARLIC_BREATH, type) - else - REMOVE_TRAIT(user, TRAIT_GARLIC_BREATH, type) - -/obj/item/clothing/head/beret/frenchberet/dropped(mob/user, silent) - . = ..() - REMOVE_TRAIT(user, TRAIT_GARLIC_BREATH, type) diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm index 1687bc221321..48bbf32999e8 100644 --- a/code/modules/clothing/head/jobs.dm +++ b/code/modules/clothing/head/jobs.dm @@ -320,7 +320,7 @@ var/obj/item/found_item = items_by_regex[found_regex] if(wearer.put_in_hands(found_item)) wearer.visible_message(span_warning("[src] drops [found_item] into the hands of [wearer]!")) - . = TRUE + . = HEAR_HEARD | HEAR_UNDERSTOOD else balloon_alert(wearer, "can't put in hands!") break diff --git a/code/modules/clothing/head/tinfoilhat.dm b/code/modules/clothing/head/tinfoilhat.dm index 18cd6fd931a1..5d5a6cc25bca 100644 --- a/code/modules/clothing/head/tinfoilhat.dm +++ b/code/modules/clothing/head/tinfoilhat.dm @@ -6,6 +6,7 @@ armor_type = /datum/armor/costume_foilhat equip_delay_other = 14 SECONDS clothing_flags = ANTI_TINFOIL_MANEUVER + clothing_traits = list(TRAIT_DONT_HEAR_PRAYERS) //stops you from hearing prayers as well, yes var/datum/brain_trauma/mild/phobia/conspiracies/paranoia var/warped = FALSE interaction_flags_mouse_drop = NEED_HANDS diff --git a/code/modules/clothing/masks/gas_filter.dm b/code/modules/clothing/masks/gas_filter.dm index 98abaf220dd2..5c1463b901bc 100644 --- a/code/modules/clothing/masks/gas_filter.dm +++ b/code/modules/clothing/masks/gas_filter.dm @@ -68,28 +68,31 @@ var/danger_points = 0 for(var/gas_id in breath.gases) + var/iterating_gas = breath.gases[gas_id] + if(!iterating_gas) + continue if(gas_id in high_filtering_gases) - if(breath.gases[gas_id][MOLES] > HIGH_FILTERING_MOLES) - breath.set_gas(gas_id, max(breath.gases[gas_id][MOLES] - filter_strength_high * filter_efficiency * HIGH_FILTERING_RATIO, 0)) + if(iterating_gas[MOLES] > HIGH_FILTERING_MOLES) + breath.set_gas(gas_id, max(iterating_gas[MOLES] - filter_strength_high * filter_efficiency * HIGH_FILTERING_RATIO, 0)) danger_points += 1 continue - breath.set_gas(gas_id, max(breath.gases[gas_id][MOLES] - filter_strength_high * filter_efficiency * LOW_FILTERING_RATIO, 0)) + breath.set_gas(gas_id, max(iterating_gas[MOLES] - filter_strength_high * filter_efficiency * LOW_FILTERING_RATIO, 0)) danger_points += 0.2 continue if(gas_id in mid_filtering_gases) - if(breath.gases[gas_id][MOLES] > MID_FILTERING_MOLES) - breath.set_gas(gas_id, max(breath.gases[gas_id][MOLES] - filter_strength_mid * filter_efficiency * HIGH_FILTERING_RATIO, 0)) + if(iterating_gas[MOLES] > MID_FILTERING_MOLES) + breath.set_gas(gas_id, max(iterating_gas[MOLES] - filter_strength_mid * filter_efficiency * HIGH_FILTERING_RATIO, 0)) danger_points += 1.25 continue - breath.set_gas(gas_id, max(breath.gases[gas_id][MOLES] - filter_strength_mid * filter_efficiency * LOW_FILTERING_RATIO, 0)) + breath.set_gas(gas_id, max(iterating_gas[MOLES] - filter_strength_mid * filter_efficiency * LOW_FILTERING_RATIO, 0)) danger_points += 0.25 continue if(gas_id in low_filtering_gases) - if(breath.gases[gas_id][MOLES] > LOW_FILTERING_MOLES) - breath.set_gas(gas_id, max(breath.gases[gas_id][MOLES] - filter_strength_low * filter_efficiency * HIGH_FILTERING_RATIO, 0)) + if(iterating_gas[MOLES] > LOW_FILTERING_MOLES) + breath.set_gas(gas_id, max(iterating_gas[MOLES] - filter_strength_low * filter_efficiency * HIGH_FILTERING_RATIO, 0)) danger_points += 1.5 continue - breath.set_gas(gas_id, max(breath.gases[gas_id][MOLES] - filter_strength_low * filter_efficiency * LOW_FILTERING_RATIO, 0)) + breath.set_gas(gas_id, max(iterating_gas[MOLES] - filter_strength_low * filter_efficiency * LOW_FILTERING_RATIO, 0)) danger_points += 0.5 continue diff --git a/code/modules/clothing/suits/ablativecoat.dm b/code/modules/clothing/suits/ablativecoat.dm index 3d4b793c6631..41114e47f182 100644 --- a/code/modules/clothing/suits/ablativecoat.dm +++ b/code/modules/clothing/suits/ablativecoat.dm @@ -7,6 +7,7 @@ flags_inv = HIDEHAIR|HIDEEARS armor_type = /datum/armor/hooded_ablative strip_delay = 3 SECONDS + clothing_traits = list(TRAIT_SECURITY_HUD) var/hit_reflect_chance = 50 /datum/armor/hooded_ablative @@ -50,11 +51,9 @@ /obj/item/clothing/suit/hooded/ablative/on_hood_up(obj/item/clothing/head/hooded/hood) . = ..() var/mob/living/carbon/user = loc - ADD_TRAIT(user, TRAIT_SECURITY_HUD, HELMET_TRAIT) balloon_alert(user, "hud enabled") /obj/item/clothing/suit/hooded/ablative/on_hood_down(obj/item/clothing/head/hooded/hood) var/mob/living/carbon/user = loc - REMOVE_TRAIT(user, TRAIT_SECURITY_HUD, HELMET_TRAIT) balloon_alert(user, "hud disabled") return ..() diff --git a/code/modules/clothing/under/_under.dm b/code/modules/clothing/under/_under.dm index fbc1ce1f7158..3431bc8851b9 100644 --- a/code/modules/clothing/under/_under.dm +++ b/code/modules/clothing/under/_under.dm @@ -351,14 +351,11 @@ return if(user && !user.temporarilyRemoveItemFromInventory(accessory)) return - if(!accessory.attach(src, user)) + if(!accessory.try_attach(src, user)) return - LAZYADD(attached_accessories, accessory) - accessory.forceMove(src) - // Allow for accessories to react to the acccessory list now - accessory.successful_attach(src) + accessory.attach(src) if(user && attach_message) balloon_alert(user, "accessory attached") diff --git a/code/modules/clothing/under/accessories/_accessories.dm b/code/modules/clothing/under/accessories/_accessories.dm index 00a2e39834b1..b40948d1e8e3 100644 --- a/code/modules/clothing/under/accessories/_accessories.dm +++ b/code/modules/clothing/under/accessories/_accessories.dm @@ -74,11 +74,11 @@ attached_to.update_accessory_overlay() /** - * Actually attach this accessory to the passed clothing article. + * Try to attach this accessory to the passed clothing article. * * The accessory is not yet within the clothing's loc at this point, this hapens after success. */ -/obj/item/clothing/accessory/proc/attach(obj/item/clothing/under/attach_to, mob/living/attacher) +/obj/item/clothing/accessory/proc/try_attach(obj/item/clothing/under/attach_to, mob/living/attacher) SHOULD_CALL_PARENT(TRUE) if(atom_storage) @@ -103,10 +103,13 @@ return TRUE -/// Called after attach is completely successful and the accessory is in the clothing's loc -/obj/item/clothing/accessory/proc/successful_attach(obj/item/clothing/under/attached_to) +/// Called after try_attach returns TRUE and thus the accessory can be finally be moved into its target +/obj/item/clothing/accessory/proc/attach(obj/item/clothing/under/attached_to) SHOULD_CALL_PARENT(TRUE) + LAZYADD(attached_to.attached_accessories, src) + forceMove(attached_to) + if(!attached_to.accessory_overlay) attached_to.accessory_overlay = mutable_appearance() attached_to.accessory_overlay.overlays += generate_accessory_overlay(attached_to) //uniform appearance will be updated by the caller @@ -179,12 +182,14 @@ /// Called when the uniform this accessory is pinned to is equipped in a valid slot /obj/item/clothing/accessory/proc/accessory_equipped(obj/item/clothing/under/clothes, mob/living/user) equipped(user, user.get_slot_by_item(clothes)) // so we get any actions, item_flags get set, etc + for(var/trait in clothing_traits) // Accessory don't have slot flags by def, but they still apply clothing traits when the suit is equipped in the right slot. + ADD_CLOTHING_TRAIT(user, trait) user.update_clothing(ITEM_SLOT_OCLOTHING|ITEM_SLOT_NECK) return /// Called when the uniform this accessory is pinned to is dropped /obj/item/clothing/accessory/proc/accessory_dropped(obj/item/clothing/under/clothes, mob/living/user) - dropped(user) + dropped(user) //This handles removing clothing traits from the user by default everytime. return /// Signal proc for [COMSIG_CLOTHING_UNDER_ADJUSTED] on the uniform we're pinned to @@ -195,8 +200,7 @@ if(can_attach_accessory(source)) return - source.remove_accessory(src) - forceMove(source.drop_location()) + forceMove(source.drop_location()) //This calls remove_accessory() source.visible_message(span_warning("[src] falls off of [source]!")) /// Signal proc for [COMSIG_ATOM_UPDATE_OVERLAYS] on the uniform we're pinned to to add our overlays to the inventory icon diff --git a/code/modules/clothing/under/accessories/badges.dm b/code/modules/clothing/under/accessories/badges.dm index a267d60217c2..bfdf9afdfb2b 100644 --- a/code/modules/clothing/under/accessories/badges.dm +++ b/code/modules/clothing/under/accessories/badges.dm @@ -131,11 +131,9 @@ . += display // Examining the clothes will display the examine message of the dogtag -/obj/item/clothing/accessory/dogtag/attach(obj/item/clothing/under/attach_to, mob/living/attacher) +/obj/item/clothing/accessory/dogtag/attach(obj/item/clothing/under/attached_to) . = ..() - if(!.) - return - RegisterSignal(attach_to, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(attached_to, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) /obj/item/clothing/accessory/dogtag/detach(obj/item/clothing/under/detach_from) . = ..() @@ -238,12 +236,13 @@ name = "subversive pin" desc = "A badge which loudly and proudly proclaims your hostility to the Nanotrasen Security Team, and authority in general." icon_state = "anti_sec" + clothing_traits = list(TRAIT_ALWAYS_WANTED) /obj/item/clothing/accessory/anti_sec_pin/Initialize(mapload) . = ..() AddComponent(/datum/component/pinnable_accessory, silent = TRUE, pinning_time = 5 SECONDS) -/obj/item/clothing/accessory/anti_sec_pin/attach(obj/item/clothing/under/attach_to, mob/living/attacher) +/obj/item/clothing/accessory/anti_sec_pin/try_attach(obj/item/clothing/under/attach_to, mob/living/attacher) . = ..() if (!. || isnull(attacher)) return @@ -254,14 +253,12 @@ /obj/item/clothing/accessory/anti_sec_pin/accessory_equipped(obj/item/clothing/under/clothes, mob/living/user) . = ..() - ADD_TRAIT(user, TRAIT_ALWAYS_WANTED, "[CLOTHING_TRAIT]_[REF(src)]") if (ishuman(user)) var/mob/living/carbon/human/human_wearer = user human_wearer.sec_hud_set_security_status() /obj/item/clothing/accessory/anti_sec_pin/accessory_dropped(obj/item/clothing/under/clothes, mob/living/user) . = ..() - REMOVE_TRAIT(user, TRAIT_ALWAYS_WANTED, "[CLOTHING_TRAIT]_[REF(src)]") if (ishuman(user)) var/mob/living/carbon/human/human_wearer = user human_wearer.sec_hud_set_security_status() diff --git a/code/modules/clothing/under/accessories/medals.dm b/code/modules/clothing/under/accessories/medals.dm index be47784d6b28..41cecdf4be03 100644 --- a/code/modules/clothing/under/accessories/medals.dm +++ b/code/modules/clothing/under/accessories/medals.dm @@ -22,7 +22,7 @@ commendation_message = tgui_input_text(user, "Reason for this commendation? It will be recorded by Nanotrasen.", "Commendation", max_length = 140) return !!commendation_message -/obj/item/clothing/accessory/medal/attach(obj/item/clothing/under/attach_to, mob/living/attacher) +/obj/item/clothing/accessory/medal/try_attach(obj/item/clothing/under/attach_to, mob/living/attacher) var/mob/living/distinguished = attach_to.loc if(isnull(attacher) || !istype(distinguished) || distinguished == attacher || awarded_to) // You can't be awarded by nothing, you can't award yourself, and you can't be awarded someone else's medal diff --git a/code/modules/emote_panel/emote_panel.dm b/code/modules/emote_panel/emote_panel.dm index c39631fbe3ce..194ce6d3a4c3 100644 --- a/code/modules/emote_panel/emote_panel.dm +++ b/code/modules/emote_panel/emote_panel.dm @@ -44,11 +44,23 @@ if(emote.message_param && use_params) emote_param = tgui_input_text(ui.user, "Add params to the emote...", emote.message_param, max_length = MAX_MESSAGE_LEN) ui.user.emote(emote_key, message = emote_param, intentional = TRUE) + if("preview_sound") + var/emote_key = params["emote_key"] + if(isnull(emote_key) || !GLOB.emote_list[emote_key]) + return + var/datum/emote/emote = GLOB.emote_list[emote_key][1] + var/emote_sound = get_sfx(emote.get_sound(ui.user)) + if(!emote_sound) + to_chat(ui.user, span_warning("Couldn't get a preview sound for [emote.name]."), type = MESSAGE_TYPE_INFO) + return + SEND_SOUND(ui.user, sound(emote_sound, volume = 75)) + to_chat(ui.user, span_warning("Previewed sound for [emote.name]."), type = MESSAGE_TYPE_INFO) /datum/emote_panel/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, "EmotePanel") + ui.set_autoupdate(FALSE) ui.open() /datum/emote_panel/ui_state(mob/user) diff --git a/code/modules/escape_menu/escape_menu.dm b/code/modules/escape_menu/escape_menu.dm index 8e86d78d6476..f1a24d249016 100644 --- a/code/modules/escape_menu/escape_menu.dm +++ b/code/modules/escape_menu/escape_menu.dm @@ -72,10 +72,12 @@ GLOBAL_LIST_EMPTY(escape_menus) START_PROCESSING(SSescape_menu, src) /datum/escape_menu/Destroy(force) + // Deleted in page holder + detail_screen = null + resource_panels = null STOP_PROCESSING(SSescape_menu, src) QDEL_NULL(base_holder) QDEL_NULL(page_holder) - resource_panels = null // list contents were already qdeled in QDEL_NULL(page_holder), so we can safely null this var/datum/our_hud = our_hud_ref?.resolve() if(our_hud) diff --git a/code/modules/events/ion_storm.dm b/code/modules/events/ion_storm.dm index 03ce7d46488c..ab1cbf970183 100644 --- a/code/modules/events/ion_storm.dm +++ b/code/modules/events/ion_storm.dm @@ -60,7 +60,7 @@ M.post_lawchange() if(botEmagChance) - for(var/mob/living/simple_animal/bot/bot in GLOB.alive_mob_list) + for(var/mob/living/basic/bot/bot in GLOB.alive_mob_list) if(prob(botEmagChance)) bot.emag_act() diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm index 8d540662bf45..1aa1665069ae 100644 --- a/code/modules/fishing/sources/_fish_source.dm +++ b/code/modules/fishing/sources/_fish_source.dm @@ -281,10 +281,11 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons()) if(!success) return var/atom/movable/reward = dispense_reward(challenge.reward_path, user, challenge.location, challenge.used_rod) - if(reward) - user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = reward.name) SEND_SIGNAL(challenge.used_rod, COMSIG_FISHING_ROD_CAUGHT_FISH, reward, user) challenge.used_rod.on_reward_caught(reward, user) + if(!reward) + return + user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = reward.name) REMOVE_TRAIT(reward, TRAIT_FISH_JUST_SPAWNED, TRAIT_GENERIC) /// Gives out the reward if possible diff --git a/code/game/objects/items/food/_food.dm b/code/modules/food_and_drinks/food/_food.dm similarity index 100% rename from code/game/objects/items/food/_food.dm rename to code/modules/food_and_drinks/food/_food.dm diff --git a/code/game/objects/items/food/bait.dm b/code/modules/food_and_drinks/food/bait.dm similarity index 100% rename from code/game/objects/items/food/bait.dm rename to code/modules/food_and_drinks/food/bait.dm diff --git a/code/game/objects/items/food/bread.dm b/code/modules/food_and_drinks/food/bread.dm similarity index 100% rename from code/game/objects/items/food/bread.dm rename to code/modules/food_and_drinks/food/bread.dm diff --git a/code/game/objects/items/food/burgers.dm b/code/modules/food_and_drinks/food/burgers.dm similarity index 100% rename from code/game/objects/items/food/burgers.dm rename to code/modules/food_and_drinks/food/burgers.dm diff --git a/code/game/objects/items/food/cake.dm b/code/modules/food_and_drinks/food/cake.dm similarity index 100% rename from code/game/objects/items/food/cake.dm rename to code/modules/food_and_drinks/food/cake.dm diff --git a/code/game/objects/items/food/cheese.dm b/code/modules/food_and_drinks/food/cheese.dm similarity index 100% rename from code/game/objects/items/food/cheese.dm rename to code/modules/food_and_drinks/food/cheese.dm diff --git a/code/game/objects/items/food/donkpocket.dm b/code/modules/food_and_drinks/food/donkpocket.dm similarity index 100% rename from code/game/objects/items/food/donkpocket.dm rename to code/modules/food_and_drinks/food/donkpocket.dm diff --git a/code/game/objects/items/food/donuts.dm b/code/modules/food_and_drinks/food/donuts.dm similarity index 100% rename from code/game/objects/items/food/donuts.dm rename to code/modules/food_and_drinks/food/donuts.dm diff --git a/code/game/objects/items/food/dough.dm b/code/modules/food_and_drinks/food/dough.dm similarity index 100% rename from code/game/objects/items/food/dough.dm rename to code/modules/food_and_drinks/food/dough.dm diff --git a/code/game/objects/items/food/egg.dm b/code/modules/food_and_drinks/food/egg.dm similarity index 100% rename from code/game/objects/items/food/egg.dm rename to code/modules/food_and_drinks/food/egg.dm diff --git a/code/game/objects/items/food/frozen.dm b/code/modules/food_and_drinks/food/frozen.dm similarity index 100% rename from code/game/objects/items/food/frozen.dm rename to code/modules/food_and_drinks/food/frozen.dm diff --git a/code/game/objects/items/food/lizard.dm b/code/modules/food_and_drinks/food/lizard.dm similarity index 100% rename from code/game/objects/items/food/lizard.dm rename to code/modules/food_and_drinks/food/lizard.dm diff --git a/code/game/objects/items/food/martian.dm b/code/modules/food_and_drinks/food/martian.dm similarity index 100% rename from code/game/objects/items/food/martian.dm rename to code/modules/food_and_drinks/food/martian.dm diff --git a/code/game/objects/items/food/meatdish.dm b/code/modules/food_and_drinks/food/meatdish.dm similarity index 100% rename from code/game/objects/items/food/meatdish.dm rename to code/modules/food_and_drinks/food/meatdish.dm diff --git a/code/game/objects/items/food/meatslab.dm b/code/modules/food_and_drinks/food/meatslab.dm similarity index 100% rename from code/game/objects/items/food/meatslab.dm rename to code/modules/food_and_drinks/food/meatslab.dm diff --git a/code/game/objects/items/food/mexican.dm b/code/modules/food_and_drinks/food/mexican.dm similarity index 100% rename from code/game/objects/items/food/mexican.dm rename to code/modules/food_and_drinks/food/mexican.dm diff --git a/code/game/objects/items/food/misc.dm b/code/modules/food_and_drinks/food/misc.dm similarity index 100% rename from code/game/objects/items/food/misc.dm rename to code/modules/food_and_drinks/food/misc.dm diff --git a/code/game/objects/items/food/monkeycube.dm b/code/modules/food_and_drinks/food/monkeycube.dm similarity index 100% rename from code/game/objects/items/food/monkeycube.dm rename to code/modules/food_and_drinks/food/monkeycube.dm diff --git a/code/game/objects/items/food/moth.dm b/code/modules/food_and_drinks/food/moth.dm similarity index 100% rename from code/game/objects/items/food/moth.dm rename to code/modules/food_and_drinks/food/moth.dm diff --git a/code/game/objects/items/food/packaged.dm b/code/modules/food_and_drinks/food/packaged.dm similarity index 100% rename from code/game/objects/items/food/packaged.dm rename to code/modules/food_and_drinks/food/packaged.dm diff --git a/code/game/objects/items/food/pancakes.dm b/code/modules/food_and_drinks/food/pancakes.dm similarity index 100% rename from code/game/objects/items/food/pancakes.dm rename to code/modules/food_and_drinks/food/pancakes.dm diff --git a/code/game/objects/items/food/pastries.dm b/code/modules/food_and_drinks/food/pastries.dm similarity index 100% rename from code/game/objects/items/food/pastries.dm rename to code/modules/food_and_drinks/food/pastries.dm diff --git a/code/game/objects/items/food/pie.dm b/code/modules/food_and_drinks/food/pie.dm similarity index 100% rename from code/game/objects/items/food/pie.dm rename to code/modules/food_and_drinks/food/pie.dm diff --git a/code/game/objects/items/food/pizza.dm b/code/modules/food_and_drinks/food/pizza.dm similarity index 100% rename from code/game/objects/items/food/pizza.dm rename to code/modules/food_and_drinks/food/pizza.dm diff --git a/code/game/objects/items/food/salad.dm b/code/modules/food_and_drinks/food/salad.dm similarity index 100% rename from code/game/objects/items/food/salad.dm rename to code/modules/food_and_drinks/food/salad.dm diff --git a/code/game/objects/items/food/sandwichtoast.dm b/code/modules/food_and_drinks/food/sandwichtoast.dm similarity index 100% rename from code/game/objects/items/food/sandwichtoast.dm rename to code/modules/food_and_drinks/food/sandwichtoast.dm diff --git a/code/game/objects/items/food/snacks.dm b/code/modules/food_and_drinks/food/snacks.dm similarity index 100% rename from code/game/objects/items/food/snacks.dm rename to code/modules/food_and_drinks/food/snacks.dm diff --git a/code/game/objects/items/food/soup.dm b/code/modules/food_and_drinks/food/soup.dm similarity index 100% rename from code/game/objects/items/food/soup.dm rename to code/modules/food_and_drinks/food/soup.dm diff --git a/code/game/objects/items/food/spaghetti.dm b/code/modules/food_and_drinks/food/spaghetti.dm similarity index 100% rename from code/game/objects/items/food/spaghetti.dm rename to code/modules/food_and_drinks/food/spaghetti.dm diff --git a/code/game/objects/items/food/sweets.dm b/code/modules/food_and_drinks/food/sweets.dm similarity index 100% rename from code/game/objects/items/food/sweets.dm rename to code/modules/food_and_drinks/food/sweets.dm diff --git a/code/game/objects/items/food/vegetables.dm b/code/modules/food_and_drinks/food/vegetables.dm similarity index 100% rename from code/game/objects/items/food/vegetables.dm rename to code/modules/food_and_drinks/food/vegetables.dm diff --git a/code/modules/food_and_drinks/recipes/drinks/drinks_alcoholic.dm b/code/modules/food_and_drinks/recipes/drinks/drinks_alcoholic.dm index 0b800fc6a87f..2f36259f0370 100644 --- a/code/modules/food_and_drinks/recipes/drinks/drinks_alcoholic.dm +++ b/code/modules/food_and_drinks/recipes/drinks/drinks_alcoholic.dm @@ -50,11 +50,11 @@ /datum/chemical_reaction/drink/gin_tonic results = list(/datum/reagent/consumable/ethanol/gintonic = 3) - required_reagents = list(/datum/reagent/consumable/ethanol/gin = 2, /datum/reagent/consumable/tonic = 1) + required_reagents = list(/datum/reagent/consumable/ethanol/gin = 1, /datum/reagent/consumable/tonic = 2) /datum/chemical_reaction/drink/rum_coke results = list(/datum/reagent/consumable/ethanol/rum_coke = 3) - required_reagents = list(/datum/reagent/consumable/ethanol/rum = 2, /datum/reagent/consumable/space_cola = 1) + required_reagents = list(/datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/consumable/space_cola = 2) /datum/chemical_reaction/drink/cuba_libre results = list(/datum/reagent/consumable/ethanol/cuba_libre = 4) @@ -75,11 +75,11 @@ /datum/chemical_reaction/drink/whiskey_cola results = list(/datum/reagent/consumable/ethanol/whiskey_cola = 3) - required_reagents = list(/datum/reagent/consumable/ethanol/whiskey = 2, /datum/reagent/consumable/space_cola = 1) + required_reagents = list(/datum/reagent/consumable/ethanol/whiskey = 1, /datum/reagent/consumable/space_cola = 2) /datum/chemical_reaction/drink/screwdriver results = list(/datum/reagent/consumable/ethanol/screwdrivercocktail = 3) - required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 2, /datum/reagent/consumable/orangejuice = 1) + required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/orangejuice = 2) /datum/chemical_reaction/drink/bloody_mary results = list(/datum/reagent/consumable/ethanol/bloody_mary = 4) @@ -185,11 +185,11 @@ /datum/chemical_reaction/drink/vodka_tonic results = list(/datum/reagent/consumable/ethanol/vodkatonic = 3) - required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 2, /datum/reagent/consumable/tonic = 1) + required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/tonic = 2) /datum/chemical_reaction/drink/gin_fizz results = list(/datum/reagent/consumable/ethanol/ginfizz = 4) - required_reagents = list(/datum/reagent/consumable/ethanol/gin = 2, /datum/reagent/consumable/sodawater = 1, /datum/reagent/consumable/limejuice = 1) + required_reagents = list(/datum/reagent/consumable/ethanol/gin = 1, /datum/reagent/consumable/sodawater = 2, /datum/reagent/consumable/limejuice = 1) /datum/chemical_reaction/drink/bahama_mama results = list(/datum/reagent/consumable/ethanol/bahama_mama = 5) diff --git a/code/modules/hydroponics/grown/beans.dm b/code/modules/hydroponics/grown/beans.dm index 51c8643113e0..2c7151a124b3 100644 --- a/code/modules/hydroponics/grown/beans.dm +++ b/code/modules/hydroponics/grown/beans.dm @@ -69,7 +69,7 @@ desc = "These seeds grow into butterbean plants." icon_state = "seed-butterbean" species = "butterbean" - plantname = "butterbean Plants" + plantname = "Butterbean Plants" product = /obj/item/food/grown/butterbeans potency = 10 mutatelist = null diff --git a/code/modules/hydroponics/grown/grass_carpet.dm b/code/modules/hydroponics/grown/grass_carpet.dm index dc1f11a85b5a..e58aaef71fc2 100644 --- a/code/modules/hydroponics/grown/grass_carpet.dm +++ b/code/modules/hydroponics/grown/grass_carpet.dm @@ -15,7 +15,8 @@ growthstages = 2 icon_grow = "grass-grow" icon_dead = "grass-dead" - genes = list(/datum/plant_gene/trait/repeated_harvest) + genes = list(/datum/plant_gene/trait/repeated_harvest, /datum/plant_gene/trait/safe_instability) + graft_gene = /datum/plant_gene/trait/safe_instability mutatelist = list(/obj/item/seeds/grass/carpet, /obj/item/seeds/grass/fairy) reagents_add = list(/datum/reagent/consumable/nutriment = 0.02, /datum/reagent/hydrogen = 0.05) @@ -53,7 +54,7 @@ product = /obj/item/food/grown/grass/fairy icon_grow = "fairygrass-grow" icon_dead = "fairygrass-dead" - genes = list(/datum/plant_gene/trait/repeated_harvest, /datum/plant_gene/trait/glow/blue) + genes = list(/datum/plant_gene/trait/repeated_harvest, /datum/plant_gene/trait/glow/blue, /datum/plant_gene/trait/safe_instability) reagents_add = list(/datum/reagent/consumable/nutriment = 0.02, /datum/reagent/hydrogen = 0.05, /datum/reagent/drug/space_drugs = 0.15) graft_gene = /datum/plant_gene/trait/glow/blue mutatelist = null diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm index 46dfeaea46c3..7311af695e21 100644 --- a/code/modules/hydroponics/hydroponics.dm +++ b/code/modules/hydroponics/hydroponics.dm @@ -851,7 +851,8 @@ continue if(T.myseed && T.plant_status != HYDROTRAY_PLANT_DEAD) T.myseed.set_potency(round((T.myseed.potency+(1/10)*(myseed.potency-T.myseed.potency)))) - T.myseed.set_instability(round((T.myseed.instability+(1/10)*(myseed.instability-T.myseed.instability)))) + if(!T.myseed.get_gene(/datum/plant_gene/trait/safe_instability)) + T.myseed.set_instability(round((T.myseed.instability+(1/10)*(myseed.instability-T.myseed.instability)))) T.myseed.set_yield(round((T.myseed.yield+(1/2)*(myseed.yield-T.myseed.yield)))) being_pollinated = TRUE add_shared_particles(/particles/pollen) @@ -1116,8 +1117,7 @@ if(!do_after(user, 2 SECONDS, src)) return - oursoil.transfer_soil(src, inside_tray = TRUE) - + current_soil = oursoil.transfer_soil(src, inside_tray = TRUE) RefreshParts() tray_flags = current_soil.tray_flags diff --git a/code/modules/hydroponics/plant_genes.dm b/code/modules/hydroponics/plant_genes.dm index 2835d49908f2..a550a644ce75 100644 --- a/code/modules/hydroponics/plant_genes.dm +++ b/code/modules/hydroponics/plant_genes.dm @@ -295,6 +295,14 @@ /datum/plant_gene/trait/slip/proc/handle_slip(obj/item/food/grown/our_plant, mob/slipped_target) SEND_SIGNAL(our_plant, COMSIG_PLANT_ON_SLIP, slipped_target) +/// Prevents instability from being changed BY cross-pollination. +/datum/plant_gene/trait/safe_instability + name = "Conserved Genetics" + description = "With highly conserved genetics, this plant doesn't lose or gain instability in cross-pollination." + icon = FA_ICON_SEEDLING + rate = 1 + mutability_flags = PLANT_GENE_REMOVABLE | PLANT_GENE_MUTATABLE | PLANT_GENE_GRAFTABLE + /* * Cell recharging trait. Charges all mob's power cells to (potency*rate)% mark when eaten. * Generates sparks on squash. diff --git a/code/modules/hydroponics/soil.dm b/code/modules/hydroponics/soil.dm index fb343bb29d08..b67c789b4266 100644 --- a/code/modules/hydroponics/soil.dm +++ b/code/modules/hydroponics/soil.dm @@ -184,7 +184,9 @@ playsound(target, placement_sound, 65, vary = TRUE) if(!inside_tray) stored_soil.on_place() + var/obj/machinery/hydroponics/soil_ref = stored_soil stored_soil.forceMove(target) //stored_soil is set to null at this point, and the soil sack is deleted when that happens + return soil_ref /obj/item/soil_sack/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE) if(attack_type == OVERWHELMING_ATTACK) diff --git a/code/modules/lost_crew/recovered_crew.dm b/code/modules/lost_crew/recovered_crew.dm index b4018b42e507..6a188029c75d 100644 --- a/code/modules/lost_crew/recovered_crew.dm +++ b/code/modules/lost_crew/recovered_crew.dm @@ -1,6 +1,7 @@ /// Revived crew ready to serve once more! Only here for tracking/admin reasons, otherwise hidden /datum/antagonist/recovered_crew name = "\improper Recovered Crew" + antagpanel_category = "Recovered Crew" show_in_antagpanel = TRUE pref_flag = ROLE_RECOVERED_CREW show_name_in_check_antagonists = TRUE diff --git a/code/modules/mapping/reader.dm b/code/modules/mapping/reader.dm index fcb5b445b412..e27984b5523e 100644 --- a/code/modules/mapping/reader.dm +++ b/code/modules/mapping/reader.dm @@ -933,6 +933,8 @@ GLOBAL_LIST_EMPTY(map_model_default) old_area.turfs_to_uncontain_by_zlevel[crds.z] += crds area_instance.turfs_by_zlevel[crds.z] += crds area_instance.contents.Add(crds) + if(!isnull(old_area) && !old_area.has_contained_turfs()) + qdel(old_area) if(GLOB.use_preloader) world.preloader_load(area_instance) diff --git a/code/modules/mob/dead/observer/observer_say.dm b/code/modules/mob/dead/observer/observer_say.dm index ac0aa48ae32b..927eabab73eb 100644 --- a/code/modules/mob/dead/observer/observer_say.dm +++ b/code/modules/mob/dead/observer/observer_say.dm @@ -82,4 +82,4 @@ to_chat(src, html = "[link] [message]", avoid_highlighting = speaker == src) - return TRUE + return HEAR_HEARD | HEAR_UNDERSTOOD diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm index b600c291214d..5c643461b159 100644 --- a/code/modules/mob/dead/observer/orbit.dm +++ b/code/modules/mob/dead/observer/orbit.dm @@ -302,7 +302,7 @@ GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new) /mob/eye, /mob/living/basic/boss, /mob/living/basic/regal_rat, - /mob/living/simple_animal/bot, + /mob/living/basic/bot, /mob/living/simple_animal/hostile/megafauna, )) if(!is_type_in_typecache(potential_mob_poi, mob_allowed_typecache) && !potential_mob_poi.GetComponent(/datum/component/deadchat_control)) diff --git a/code/modules/mob/living/basic/bots/_bots.dm b/code/modules/mob/living/basic/bots/_bots.dm index 15d983b8745c..f7962d9b8687 100644 --- a/code/modules/mob/living/basic/bots/_bots.dm +++ b/code/modules/mob/living/basic/bots/_bots.dm @@ -196,7 +196,7 @@ GLOBAL_LIST_INIT(command_strings, list( /mob/living/basic/bot/proc/get_emagged_message() return get_policy(ROLE_EMAGGED_BOT) || "You are a malfunctioning bot! Disrupt everyone and cause chaos!" -/mob/living/basic/bot/proc/turn_on() +/mob/living/basic/bot/proc/turn_on(mob/user) if(stat == DEAD) return FALSE set_mode_flags(bot_mode_flags | BOT_MODE_ON) diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot.dm new file mode 100644 index 000000000000..55d5632e5e18 --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot.dm @@ -0,0 +1,193 @@ +/mob/living/basic/bot/mulebot + name = "\improper MULEbot" + desc = "A Multiple Utility Load Effector bot." + icon = 'icons/mob/silicon/aibots.dmi' + icon_state = "mulebot0" + base_icon_state = "mulebot" + + light_color = "#ffcc99" + light_power = 0.8 + + health = 50 + maxHealth = 50 + + damage_coeff = list(BRUTE = 0.5, BURN = 0.7, TOX = 0, STAMINA = 0, OXY = 0) + density = TRUE + mob_size = MOB_SIZE_LARGE + move_resist = MOVE_FORCE_STRONG + animate_movement = SLIDE_STEPS + speed = 3 + + combat_mode = TRUE + + buckle_lying = 0 + buckle_prevents_pull = TRUE // No pulling loaded shit + + bot_mode_flags = ~BOT_MODE_ROUNDSTART_POSSESSION + req_one_access = list(ACCESS_ROBOTICS, ACCESS_CARGO) + radio_key = /obj/item/encryptionkey/headset_cargo + radio_channel = RADIO_CHANNEL_SUPPLY + pass_flags = PASSFLAPS + bot_type = MULE_BOT + + additional_access = /datum/id_trim/job/cargo_technician + path_image_color = "#7F5200" + hud_type = /datum/hud/living/mulebot + + hackables = "obstacle detection circuits" + possessed_message = "You are a MULEbot! Do your best to make sure that packages get to their destination!" + ai_controller = /datum/ai_controller/basic_controller/bot/mulebot + + /// unique identifier in case there are multiple mulebots. + var/id + + /// what we're transporting + var/atom/movable/load + /// who's riding us + var/mob/living/passenger + + ///flags of mulebot mode + var/mulebot_delivery_flags = MULEBOT_RETURN_MODE | MULEBOT_AUTO_PICKUP_MODE | MULEBOT_REPORT_DELIVERY_MODE + + ///Internal Powercell + var/obj/item/stock_parts/power_store/cell + ///How much power we use when we move. + var/cell_move_power_usage = 0.0005 * STANDARD_CELL_CHARGE + ///The amount of steps we should take until we rest for a time. + var/num_steps = 0 + + ///The chance to be deleted and replaced by a different mule + var/replacement_chance = 0.666 + ///home destination, only used by mappers. + var/home_destination = "" + +/mob/living/basic/bot/mulebot/Initialize(mapload) + . = ..() + + if(prob(replacement_chance) && mapload) + new /mob/living/basic/bot/mulebot/paranormal(loc) + return INITIALIZE_HINT_QDEL + + set_wires(new /datum/wires/mulebot(src)) + var/obj/item/stock_parts/power_store/cell/upgraded/new_cell = new(src) + assign_cell(new_cell) + ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, "") + AddElement(/datum/element/ridable, /datum/component/riding/creature/mulebot) + ADD_TRAIT(src, TRAIT_NOMOBSWAP, INNATE_TRAIT) + add_traits(list(TRAIT_NOMOBSWAP, TRAIT_COMBAT_MODE_LOCK), INNATE_TRAIT) + RegisterSignal(src, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_pre_move)) + + set_id(suffix || assign_random_name()) + suffix = null + if(name == "\improper MULEbot") + name = "\improper MULEbot [id]" + set_home(get_turf(src)) + ai_controller.update_able_to_run() + update_appearance() + +/mob/living/basic/bot/mulebot/Destroy() + UnregisterSignal(src, COMSIG_MOVABLE_PRE_MOVE) + unload() + QDEL_NULL(cell) + return ..() + +/mob/living/basic/bot/mulebot/proc/assign_cell(atom/new_cell) + cell = new_cell + var/atom/movable/screen/mob_charge/charge_hud = hud_used?.screen_objects[HUD_MULEBOT_CHARGE] + charge_hud?.update_battery_overlay(new_cell) + charge_hud?.calculate_charge() + + +/mob/living/basic/bot/mulebot/attack_hand(mob/living/carbon/human/user, list/modifiers) + if(bot_access_flags & BOT_COVER_MAINTS_OPEN && !HAS_AI_ACCESS(user)) + wires.interact(user) + return + if(wires.is_cut(WIRE_RX) && HAS_AI_ACCESS(user)) + return + + return ..() + +/mob/living/basic/bot/mulebot/examine(mob/user) + . = ..() + if(bot_access_flags & BOT_COVER_MAINTS_OPEN) + if(cell) + . += span_notice("It has \a [cell] installed.") + . += span_info("You can use a crowbar to remove it.") + else + . += span_notice("It has an empty compartment where a power cell can be installed.") + if(load) //observer check is so we don't show the name of the ghost that's sitting on it to prevent metagaming who's ded. + . += span_notice("\A [isobserver(load) ? "ghostly figure" : load] is on its load platform.") + +/mob/living/basic/bot/mulebot/get_cell() + return cell + +/mob/living/basic/bot/mulebot/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE) + if(!can_unarmed_attack()) + return + if(isturf(target) && isturf(loc) && loc.Adjacent(target) && load) + unload(get_dir(loc, target)) + else + return ..() + +/mob/living/basic/bot/mulebot/turn_on(mob/user) + if(bot_access_flags & BOT_COVER_MAINTS_OPEN) + if(user) + to_chat(user, span_warning("[src]'s maintenance panel is open!")) + return FALSE + if(!has_power()) + if(user) + to_chat(user, span_warning("[src] has no power!")) + return FALSE + return ..() + +/mob/living/basic/bot/mulebot/update_icon_state() //if you change the icon_state names, please make sure to update /datum/wires/mulebot/on_pulse() as well. <3 + . = ..() + icon_state = "[base_icon_state][(bot_mode_flags & BOT_MODE_ON) ? wires?.is_cut(WIRE_AVOIDANCE) : "0"]" + +/mob/living/basic/bot/mulebot/update_overlays() + . = ..() + if(bot_access_flags & BOT_COVER_MAINTS_OPEN) + . += "[base_icon_state]-hatch" + if(isnull(load) || ismob(load)) //mob offsets and such are handled by the riding component / buckling + return + var/mutable_appearance/load_overlay = mutable_appearance(load.icon, load.icon_state, layer + 0.01) + load_overlay.pixel_y = initial(load.pixel_y) + 11 + . += load_overlay + +/mob/living/basic/bot/mulebot/proc/handle_buzzing(datum/move_loop/has_target/jps/frustrations/source, frustration_counter) + SIGNAL_HANDLER + + update_bot_mode(new_mode = BOT_BLOCKED) + var/buzz_mode = frustration_counter >= source.maximum_frustrations ? MULEBOT_MOOD_ANNOYED : MULEBOT_MOOD_SIGH + buzz(buzz_mode) + +/mob/living/basic/bot/mulebot/handle_loop_movement(atom/movable/source, atom/oldloc, dir, forced) //incase we start moving again after being previously blocked, update our mode + . = ..() + if(mode != BOT_BLOCKED) + return + var/obj/machinery/navbeacon/beacon = ai_controller.current_movement_target + if(!istype(beacon)) + return + var/intended_mode = beacon.location == ai_controller.blackboard[BB_MULEBOT_HOME_BEACON] ? BOT_GO_HOME : BOT_DELIVER + update_bot_mode(new_mode = intended_mode) + +///Noises that mulebots make +/mob/living/basic/bot/mulebot/proc/buzz(type) + switch(type) + if(MULEBOT_MOOD_SIGH) + audible_message(span_hear("[src] makes a sighing buzz.")) + playsound(src, 'sound/machines/buzz/buzz-sigh.ogg', 50, FALSE) + if(MULEBOT_MOOD_ANNOYED) + audible_message(span_hear("[src] makes an annoyed buzzing sound.")) + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, FALSE) + if(MULEBOT_MOOD_DELIGHT) + audible_message(span_hear("[src] makes a delighted ping!")) + playsound(src, 'sound/machines/ping.ogg', 50, FALSE) + if(MULEBOT_MOOD_CHIME) + audible_message(span_hear("[src] makes a chiming sound!")) + playsound(src, 'sound/machines/chime.ogg', 50, FALSE) + flick("[base_icon_state]1", src) + +/// returns true if the bot is fully powered. +/mob/living/basic/bot/mulebot/proc/has_power() + return cell && cell.charge > 0 && (!wires.is_cut(WIRE_POWER1) && !wires.is_cut(WIRE_POWER2)) diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_ai.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_ai.dm new file mode 100644 index 000000000000..7898ced148ce --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_ai.dm @@ -0,0 +1,145 @@ +/datum/ai_controller/basic_controller/bot/mulebot + blackboard = list( + BB_SALUTE_MESSAGES = list( + "blinks its light in appreciation towards", + ) + ) + ai_movement = /datum/ai_movement/jps/bot/mulebot + max_target_distance = AI_MULEBOT_PATH_LENGTH + planning_subtrees = list( + /datum/ai_planning_subtree/respond_to_summon, + /datum/ai_planning_subtree/salute_authority, + /datum/ai_planning_subtree/attempt_delivery, + /datum/ai_planning_subtree/find_delivery_beacon, + ) + reset_keys = list( + BB_BOT_SUMMON_TARGET, + BB_MULEBOT_DESTINATION_BEACON, + BB_MULEBOT_TRAVEL_TARGET, + ) + +/datum/ai_controller/basic_controller/bot/mulebot/get_able_to_run() + var/mob/living/basic/bot/mulebot/bot_pawn = pawn + if(!bot_pawn.has_power()) + return AI_UNABLE_TO_RUN + return ..() + +/datum/ai_controller/basic_controller/bot/mulebot/setup_able_to_run() + . = ..() + var/mob/living/basic/bot/my_bot = pawn + var/static/list/wire_signals = list( + COMSIG_MEND_WIRE(WIRE_POWER1), //this framework is insane + COMSIG_MEND_WIRE(WIRE_POWER2), + COMSIG_CUT_WIRE(WIRE_POWER1), + COMSIG_CUT_WIRE(WIRE_POWER2), + ) + RegisterSignals(my_bot.wires, wire_signals, PROC_REF(update_able_to_run)) + var/static/list/content_signals = list( + COMSIG_ATOM_ENTERED, + COMSIG_ATOM_EXITED, + ) + RegisterSignals(my_bot, content_signals, PROC_REF(update_able_to_run)) + +/datum/ai_planning_subtree/find_delivery_beacon + ///what behavior do we use to seek beacons + var/find_beacon_behaviour = /datum/ai_behavior/find_delivery_beacon + +/datum/ai_planning_subtree/find_delivery_beacon/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn + if(bot_pawn.wires.is_cut(WIRE_BEACON)) + return + + if(!controller.blackboard_key_exists(BB_MULEBOT_TRAVEL_TARGET)) + controller.queue_behavior(find_beacon_behaviour, BB_MULEBOT_TRAVEL_TARGET) + +/datum/ai_behavior/find_delivery_beacon + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/find_delivery_beacon/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn + var/atom/delivery_beacon + + var/beacon_tag = null + + switch(bot_pawn.mode) + if(BOT_DELIVER) + beacon_tag = controller.blackboard[BB_MULEBOT_DESTINATION_BEACON] + if(BOT_GO_HOME) + beacon_tag = controller.blackboard[BB_MULEBOT_HOME_BEACON] + else + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + for(var/obj/machinery/navbeacon/beacon as anything in GLOB.deliverybeacons) + if(beacon.location == beacon_tag) + delivery_beacon = beacon + break + + if(isnull(delivery_beacon)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + controller.set_blackboard_key(BB_MULEBOT_TRAVEL_TARGET, delivery_beacon) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/travel_towards/delivery_beacon + new_movement_type = /datum/ai_movement/jps/bot/mulebot + +/datum/ai_planning_subtree/attempt_delivery + ///behavior we use to unload crates + var/delivery_behaviour = /datum/ai_behavior/handle_delivery + +/datum/ai_planning_subtree/attempt_delivery/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard_key_exists(BB_MULEBOT_TRAVEL_TARGET)) + return + + controller.queue_behavior(delivery_behaviour, BB_MULEBOT_TRAVEL_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/handle_delivery + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/handle_delivery/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/handle_delivery/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/obj/machinery/navbeacon/beacon = controller.blackboard[target_key] + var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn + + var/load_direction = beacon.codes[NAVBEACON_DELIVERY_DIRECTION] // this will be the load/unload dir + if(!load_direction) + load_direction = beacon.dir // fallback + + load_direction = text2num(load_direction) + + if(bot_pawn.load) + if(bot_pawn.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE) + bot_pawn.radio_channel = RADIO_CHANNEL_SUPPLY //Supply channel + bot_pawn.buzz(MULEBOT_MOOD_CHIME) + bot_pawn.speak("Destination [RUNECHAT_BOLD("[beacon.location]")] reached. Unloading [bot_pawn.load].", bot_pawn.radio_channel) + bot_pawn.unload(load_direction) + + else + if(bot_pawn.mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE) // find a crate + var/atom/movable/atom_to_pick_up + if(bot_pawn.wires.is_cut(WIRE_LOADCHECK)) // if hacked, load first unanchored thing we find + for(var/atom/movable/target_atom in get_step(bot_pawn.loc, load_direction)) + if(!target_atom.anchored) + atom_to_pick_up = target_atom + break + else // otherwise, look for crates only + atom_to_pick_up = locate(/obj/structure/closet/crate) in get_step(bot_pawn.loc, load_direction) + if(atom_to_pick_up?.Adjacent(bot_pawn)) + bot_pawn.load(atom_to_pick_up) + if(bot_pawn.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE) + bot_pawn.speak("Now loading [bot_pawn.load] at [RUNECHAT_BOLD("[get_area_name(bot_pawn)]")].", bot_pawn.radio_channel) + + if((bot_pawn.mulebot_delivery_flags & MULEBOT_RETURN_MODE) && controller.blackboard[BB_MULEBOT_HOME_BEACON] && controller.blackboard[BB_MULEBOT_HOME_BEACON] != beacon.location) + bot_pawn.update_bot_mode(new_mode = BOT_GO_HOME) + controller.clear_blackboard_key(BB_MULEBOT_TRAVEL_TARGET) + else + bot_pawn.bot_reset() // otherwise go idle + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm new file mode 100644 index 000000000000..d7bd93be22c4 --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm @@ -0,0 +1,116 @@ +/mob/living/basic/bot/mulebot/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Mule", name) + ui.open() + +/mob/living/basic/bot/mulebot/ui_data(mob/user) + var/list/data = list() + data["powerStatus"] = bot_mode_flags & BOT_MODE_ON + data["locked"] = bot_access_flags & BOT_COVER_LOCKED + data["siliconUser"] = HAS_SILICON_ACCESS(user) + data["mode"] = mode ? "[mode]" : "Ready" + data["modeStatus"] = "" + switch(mode) + if(BOT_IDLE, BOT_DELIVER, BOT_GO_HOME) + data["modeStatus"] = "good" + if(BOT_BLOCKED, BOT_NAV, BOT_WAIT_FOR_NAV) + data["modeStatus"] = "average" + if(BOT_NO_ROUTE) + data["modeStatus"] = "bad" + data["load"] = get_load_name() + data["destination"] = ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON] + data["homeDestination"] = ai_controller.blackboard[BB_MULEBOT_HOME_BEACON] + data["destinationsList"] = GLOB.deliverybeacontags + data["cellPercent"] = cell?.percent() + data["autoReturn"] = mulebot_delivery_flags & MULEBOT_RETURN_MODE + data["autoPickup"] = mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE + data["reportDelivery"] = mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE + data["botId"] = id + data["allowPossession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT + data["possessionEnabled"] = can_be_possessed + data["paiInserted"] = !!paicard + return data + +/mob/living/basic/bot/mulebot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + var/mob/user = ui.user + if(. || (bot_access_flags & BOT_COVER_LOCKED && !HAS_SILICON_ACCESS(user))) + return + + bot_control(action, user, params) + return TRUE + +/mob/living/basic/bot/mulebot/bot_control(command, mob/user, list/params = list(), pda = FALSE) + if(pda && wires.is_cut(WIRE_RX)) // MULE wireless is controlled by wires. + return + + switch(command) + if("stop") + if(mode != BOT_IDLE) + bot_reset() + if("go") + if(mode == BOT_IDLE) + start() + if("home") + if(mode == BOT_IDLE || mode == BOT_DELIVER) + start_home() + if("destination") + var/new_dest + if(pda) + new_dest = tgui_input_list(user, "Enter Destination", "Mulebot Settings", GLOB.deliverybeacontags, ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON]) + else + new_dest = params["value"] + if(new_dest) + set_destination(new_dest) + if("setid") + var/new_id = tgui_input_text(user, "Enter ID", "ID Assignment", id, max_length = MAX_NAME_LEN) + if(new_id) + set_id(new_id) + name = "\improper MULEbot [new_id]" + if("sethome") + var/new_home = tgui_input_list(user, "Enter Home", "Mulebot Settings", GLOB.deliverybeacontags, ai_controller.blackboard[BB_MULEBOT_HOME_BEACON]) + if(new_home) + ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, new_home) + if("unload") + if(load && mode != BOT_HUNT) + unload() + if("autoret") + mulebot_delivery_flags ^= MULEBOT_RETURN_MODE + if("autopick") + mulebot_delivery_flags ^= MULEBOT_AUTO_PICKUP_MODE + if("report") + mulebot_delivery_flags ^= MULEBOT_REPORT_DELIVERY_MODE + +/mob/living/basic/bot/mulebot/proc/start() + if(!(bot_mode_flags & BOT_MODE_ON)) + return + if(ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON] == ai_controller.blackboard[BB_MULEBOT_HOME_BEACON]) + update_bot_mode(new_mode = BOT_GO_HOME) + else + update_bot_mode(new_mode = BOT_DELIVER) + +/mob/living/basic/bot/mulebot/proc/start_home() + set_destination(ai_controller.blackboard[BB_MULEBOT_HOME_BEACON]) + update_bot_mode(new_mode = BOT_GO_HOME) + +/mob/living/basic/bot/mulebot/proc/set_destination(new_destination) + ai_controller.set_blackboard_key(BB_MULEBOT_DESTINATION_BEACON, new_destination) + +/mob/living/basic/bot/mulebot/proc/set_home(turf/home_loc) + if(home_destination) + ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, home_destination) + home_destination = null + if(!istype(home_loc)) + CRASH("MULEbot [id] was requested to set a home location to [home_loc ? "an invalid home loc ([home_loc.type])" : "null"]") + + var/obj/machinery/navbeacon/home_beacon = locate() in home_loc + if(isnull(home_beacon)) + ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, "") + return + ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, home_beacon.location) + log_transport("[id]: MULEbot successfuly set home location to ID [home_beacon.location] at [home_beacon.x], [home_beacon.y], [home_beacon.z]") + +///Sets the new ID of the mulebot +/mob/living/basic/bot/mulebot/proc/set_id(new_id) + id = new_id diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm new file mode 100644 index 000000000000..5fec23e9bbfc --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm @@ -0,0 +1,117 @@ +/mob/living/basic/bot/mulebot/execute_resist() + . = ..() + if(load) + unload() + +/mob/living/basic/bot/mulebot/Exited(atom/movable/gone, direction) + . = ..() + if(gone == load) + unload() + if(gone == cell) + turn_off() + assign_cell() + cell = null + set_cell_hud() + +/mob/living/basic/bot/mulebot/Entered(obj/item/stock_parts/power_store/cell/arrived, atom/old_loc, list/atom/old_locs) + . = ..() + if(istype(arrived) && isnull(cell)) + assign_cell(arrived) + +// mousedrop a crate to load the bot +// can load anything if hacked +/mob/living/basic/bot/mulebot/mouse_drop_receive(atom/movable/atom_to_load, mob/user, params) + if(!isliving(user)) + return + + if(!istype(atom_to_load) || isdead(atom_to_load) || iseyemob(atom_to_load) || istype(atom_to_load, /obj/effect/dummy/phased_mob)) + return + + load(atom_to_load) + +/mob/living/basic/bot/mulebot/post_unbuckle_mob(mob/living/M) + load = null + return ..() + +/mob/living/basic/bot/mulebot/relaymove(mob/living/user, direction) + if(user.incapacitated) + return + if(load == user) + unload() + +/mob/living/basic/bot/mulebot/remove_air(amount) //To prevent riders suffocating + return loc ? loc.remove_air(amount) : null + +/// Called to load an atom on the mulebot, which is usually a crate, unless if hacked +/mob/living/basic/bot/mulebot/proc/load(atom/movable/atom_to_load) + if(load || atom_to_load.anchored) + return + + if(!isturf(atom_to_load.loc)) //To prevent the loading from stuff from someone's inventory or screen icons. + return + + var/obj/structure/closet/crate/crate = atom_to_load + if(!istype(crate)) + if(!wires.is_cut(WIRE_LOADCHECK)) + buzz(MULEBOT_MOOD_SIGH) + return // if not hacked, only allow crates to be loaded + crate = null + + if(crate || isobj(atom_to_load)) + var/obj/object_to_load = atom_to_load + if(object_to_load.has_buckled_mobs() || (locate(/mob) in atom_to_load)) //can't load non crates objects with mobs buckled to it or inside it. + buzz(MULEBOT_MOOD_SIGH) + return + + if(crate) + crate.close() //make sure the crate is closed + + object_to_load.forceMove(src) + + else if(isliving(atom_to_load)) + if(!load_mob(atom_to_load)) //forceMove() is handled in buckling + return + + load = atom_to_load + update_appearance() + +///resolves the name to display for the loaded mob. primarily needed for the paranormal subtype since we don't want to show the name of ghosts riding it. +/mob/living/basic/bot/mulebot/proc/get_load_name() + return load ? load.name : null + +///Loads a mob onto the mulebot +/mob/living/basic/bot/mulebot/proc/load_mob(mob/living/mob_to_load) + can_buckle = TRUE + if(buckle_mob(mob_to_load)) + passenger = mob_to_load + load = mob_to_load + can_buckle = FALSE + return TRUE + +// called to unload the bot +// argument is optional direction to unload +// if zero or null, unload at bot's location +/mob/living/basic/bot/mulebot/proc/unload(dirn) + if(QDELETED(load)) + if(load) //if our thing was qdel'd, there's likely a leftover reference. just clear it and remove the overlay. we'll let the bot keep moving around to prevent it abruptly stopping somewhere. + load = null + update_appearance() + return + + update_bot_mode(new_mode = BOT_IDLE) + + var/atom/movable/cached_load = load //cache the load since unbuckling mobs clears the var. + + unbuckle_all_mobs() + + if(load) //don't have to do any of this for mobs. + cached_load.forceMove(loc) + cached_load.pixel_y = initial(cached_load.pixel_y) + cached_load.layer = initial(cached_load.layer) + SET_PLANE_EXPLICIT(cached_load, initial(cached_load.plane), src) + load = null + + if(dirn) //move the thing to the delivery point. + cached_load.Move(get_step(loc,dirn), dirn) + + update_appearance() diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_hud.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_hud.dm new file mode 100644 index 000000000000..3e1f26c85849 --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_hud.dm @@ -0,0 +1,74 @@ +/mob/living/basic/bot/mulebot/proc/set_cell_hud() + if(!has_power()) + set_hud_image_state(DIAG_BATT_HUD, "hudnobatt") + return + + var/atom/movable/screen/mob_charge/charge_hud = hud_used?.screen_objects[HUD_MULEBOT_CHARGE] + charge_hud?.calculate_charge() + set_hud_image_state(DIAG_BATT_HUD, "hudbatt[RoundDiagBar(cell.charge/cell.maxcharge)]") + +/atom/movable/screen/mob_charge + icon = 'icons/obj/machines/cell_charger.dmi' + icon_state = "ccharger" + screen_loc = ui_stamina + ///used to find the overlay for charger icon + var/current_charge_level = 4 + ///dynamic, based on what cell our nulebot's using + var/image/battery_overlay + ///maptext that displays charge in numbers + var/image/charge_overlay + ///is there a mouse on us + var/hovering = FALSE + +/atom/movable/screen/mob_charge/proc/update_battery_overlay(atom/target_battery) + var/obj/item/stock_parts/power_store/cell/my_cell = target_battery || (locate() in get_mob()) + if(isnull(my_cell)) + battery_overlay = null + else + battery_overlay = image(icon = my_cell.icon, icon_state = my_cell.icon_state, loc = src, layer = src.layer + 0.1) + update_appearance(UPDATE_ICON) + +/atom/movable/screen/mob_charge/proc/calculate_charge() + var/obj/item/stock_parts/power_store/cell/my_battery = locate() in get_mob() + var/charge_value = isnull(my_battery) ? 0 : round(my_battery.charge/my_battery.maxcharge * 100 , 1) + current_charge_level = round(charge_value * 4 / 100) + charge_overlay.maptext = MAPTEXT("
[charge_value]%
") + update_appearance(UPDATE_ICON) + +/atom/movable/screen/mob_charge/New(loc, ...) + . = ..() + charge_overlay = image(loc = src, layer = src.layer+0.2, pixel_y = -5) + update_battery_overlay() + +/atom/movable/screen/mob_charge/Destroy() + charge_overlay = null + battery_overlay = null + return ..() + +/atom/movable/screen/mob_charge/update_overlays() + . = ..() + . += mutable_appearance(icon, "ccharger-o[current_charge_level]") + if(battery_overlay) + . |= battery_overlay + if(hovering) + . |= charge_overlay + +/atom/movable/screen/mob_charge/MouseEntered(location,control,params) + if(usr != get_mob()) + return + . = ..() + hovering = TRUE + calculate_charge() + +/atom/movable/screen/mob_charge/MouseExited(location, control, params) + if(usr != get_mob()) + return + . = ..() + hovering = FALSE + update_appearance(UPDATE_ICON) + +/datum/hud/living/mulebot + +/datum/hud/living/mulebot/initialize_screen_objects() + . = ..() + add_screen_object(/atom/movable/screen/mob_charge, HUD_MULEBOT_CHARGE, HUD_GROUP_INFO) diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_movement.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_movement.dm new file mode 100644 index 000000000000..0654477a0965 --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_movement.dm @@ -0,0 +1,72 @@ +/mob/living/basic/bot/mulebot/MobBump(mob/bumped_mob) // called when the bot bumps into a mob + if(mind || !isliving(bumped_mob)) //if there's a sentience controlling the bot, they aren't allowed to harm folks. + return ..() + var/mob/living/bumped_living = bumped_mob + if(wires.is_cut(WIRE_AVOIDANCE)) // usually just bumps, but if the avoidance wire is cut, knocks them over. + if(iscyborg(bumped_living)) + visible_message(span_danger("[src] bumps into [bumped_living]!")) + else if(bumped_living.Knockdown(8 SECONDS)) + log_combat(src, bumped_living, "knocked down") + visible_message(span_danger("[src] knocks over [bumped_living]!")) + return ..() + +/mob/living/basic/bot/mulebot/on_bot_movement(atom/movable/source, atom/oldloc, dir, forced) + cell?.use(cell_move_power_usage) + set_cell_hud() + + if(has_gravity()) + for(var/mob/living/carbon/human/future_pancake in loc) + if(future_pancake.body_position == LYING_DOWN) + run_over(future_pancake) + + return ..() + +///Checks if the bot is on or if it has charge +/mob/living/basic/bot/mulebot/proc/on_pre_move() + SIGNAL_HANDLER + + if(!(bot_mode_flags & BOT_MODE_ON)) + return COMPONENT_MOB_BOT_BLOCK_PRE_STEP + + if((cell && (cell.charge < cell_move_power_usage)) || !has_power()) + turn_off() + return COMPONENT_MOB_BOT_BLOCK_PRE_STEP + +// when mulebot is in the same loc +/mob/living/basic/bot/mulebot/proc/run_over(mob/living/carbon/human/crushed) + if (!(bot_access_flags & BOT_COVER_EMAGGED) && !wires.is_cut(WIRE_AVOIDANCE)) + if (!has_status_effect(/datum/status_effect/careful_driving)) + crushed.visible_message(span_notice("[src] slows down to avoid crushing [crushed].")) + apply_status_effect(/datum/status_effect/careful_driving) + return // Player mules must be emagged before they can trample + + log_combat(src, crushed, "run over", addition = "(DAMTYPE: [uppertext(BRUTE)])") + crushed.visible_message( + span_danger("[src] drives over [crushed]!"), + span_userdanger("[src] drives over you!"), + ) + + playsound(src, 'sound/effects/splat.ogg', 50, TRUE) + + var/damage = rand(5, 15) + var/static/list/zone_damages = list( + BODY_ZONE_HEAD = 2, + BODY_ZONE_CHEST = 2, + BODY_ZONE_L_LEG = 0.5, + BODY_ZONE_R_LEG = 0.5, + BODY_ZONE_L_ARM = 0.5, + BODY_ZONE_R_ARM = 0.5, + ) + for(var/body_zone in zone_damages) + crushed.apply_damage(zone_damages[body_zone] * damage, BRUTE, body_zone, run_armor_check(body_zone, MELEE)) + + add_mob_blood(crushed) + + var/turf/below_us = get_turf(src) + below_us.add_mob_blood(crushed) + + AddComponent(/datum/component/blood_walk, \ + blood_type = /obj/effect/decal/cleanable/blood/tracks, \ + target_dir_change = TRUE, \ + transfer_blood_dna = TRUE, \ + max_blood = 4) diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_paranormal.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_paranormal.dm new file mode 100644 index 000000000000..9a4b20b9e5a3 --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_paranormal.dm @@ -0,0 +1,61 @@ +/mob/living/basic/bot/mulebot/paranormal + name = "\improper GHOULbot" + desc = "A rather ghastly looking... Multiple Utility Load Effector bot? It only seems to accept paranormal forces, and for this reason is fucking useless." + icon_state = "paranormalmulebot0" + base_icon_state = "paranormalmulebot" + ///avoid the utterly miniscule chance of infinite looping + replacement_chance = 0 + +/mob/living/basic/bot/mulebot/paranormal/update_overlays() + . = ..() + if(!isobserver(load)) + return + var/mutable_appearance/ghost_overlay = mutable_appearance('icons/mob/simple/mob.dmi', "ghost", layer + 0.01) //use a generic ghost icon, otherwise you can metagame who's dead if they have a custom ghost set + ghost_overlay.pixel_y = 12 + . += ghost_overlay + +/mob/living/basic/bot/mulebot/paranormal/get_load_name() //Don't reveal the name of ghosts so we can't metagame who died and all that. + . = ..() + if(. && isobserver(load)) + return "Unknown" + +/mob/living/basic/bot/mulebot/paranormal/load(atom/movable/movable_atom) + if(load || movable_atom.anchored) + return + + if(!isturf(movable_atom.loc)) //To prevent the loading from stuff from someone's inventory or screen icons. + return + + if(isobserver(movable_atom)) + visible_message(span_warning("A ghostly figure appears on [src]!")) + movable_atom.forceMove(src) + RegisterSignal(movable_atom, COMSIG_MOVABLE_MOVED, PROC_REF(ghost_moved)) + + else if(!wires.is_cut(WIRE_LOADCHECK)) + buzz(MULEBOT_MOOD_SIGH) + return // if not hacked, only allow ghosts to be loaded + + else if(isobj(movable_atom)) + if(movable_atom.has_buckled_mobs() || (locate(/mob) in movable_atom)) //can't load non crates objects with mobs buckled to it or inside it. + buzz(MULEBOT_MOOD_SIGH) + return + + if(istype(movable_atom, /obj/structure/closet/crate)) + var/obj/structure/closet/crate/crate = movable_atom + crate.close() //make sure it's closed + + movable_atom.forceMove(src) + + else if(isliving(movable_atom) && !load_mob(movable_atom)) + return + + load = movable_atom + update_bot_mode(new_mode = BOT_IDLE) + update_appearance() + +///Handles the ghosts moving out from the mule +/mob/living/basic/bot/mulebot/paranormal/proc/ghost_moved() + SIGNAL_HANDLER + visible_message(span_notice("The ghostly figure vanishes...")) + UnregisterSignal(load, COMSIG_MOVABLE_MOVED) + unload() diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_tool_interactions.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_tool_interactions.dm new file mode 100644 index 000000000000..d80cc520bbb8 --- /dev/null +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_tool_interactions.dm @@ -0,0 +1,46 @@ +/mob/living/basic/bot/mulebot/screwdriver_act(mob/living/user, obj/item/tool) + . = ..() + update_appearance() + +/mob/living/basic/bot/mulebot/crowbar_act(mob/living/user, obj/item/tool) + if(!(bot_access_flags & BOT_COVER_MAINTS_OPEN) || user.combat_mode) + return + if(!cell) + to_chat(user, span_warning("[src] doesn't have a power cell!")) + return ITEM_INTERACT_BLOCKING + cell.add_fingerprint(user) + user.visible_message( + span_notice("[user] crowbars [cell] out from [src]."), + span_notice("You pry [cell] out of [src]."), + ) + if(Adjacent(user) && !issilicon(user)) + user.put_in_hands(cell) + else + cell.forceMove(drop_location()) + return ITEM_INTERACT_SUCCESS + +/mob/living/basic/bot/mulebot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(istype(tool, /obj/item/stock_parts/power_store/cell) && (bot_access_flags & BOT_COVER_MAINTS_OPEN)) + if(cell) + to_chat(user, span_warning("[src] already has a power cell!")) + return ITEM_INTERACT_BLOCKING + if(!user.transferItemToLoc(tool, src)) + return ITEM_INTERACT_BLOCKING + user.visible_message( + span_notice("[user] inserts \a [cell] into [src]."), + span_notice("You insert [cell] into [src]."), + ) + return ITEM_INTERACT_SUCCESS + if(is_wire_tool(tool) && (bot_access_flags & BOT_COVER_MAINTS_OPEN)) + attack_hand(user) + return ITEM_INTERACT_SUCCESS + return ..() + + +/mob/living/basic/bot/mulebot/emag_act(mob/user, obj/item/card/emag/emag_card) + . = ..() + if(!(bot_access_flags & BOT_COVER_EMAGGED)) + return + flick("[base_icon_state]-emagged", src) + playsound(src, SFX_SPARKS, 100, FALSE, SHORT_RANGE_SOUND_EXTRARANGE) + return TRUE diff --git a/code/modules/mob/living/basic/bots/repairbot/repairbot.dm b/code/modules/mob/living/basic/bots/repairbot/repairbot.dm index 8cc3a90a3368..0e3647adb191 100644 --- a/code/modules/mob/living/basic/bots/repairbot/repairbot.dm +++ b/code/modules/mob/living/basic/bots/repairbot/repairbot.dm @@ -250,7 +250,7 @@ var/obj/item/stack/rods/new_rods = new() new_rods.forceMove(src) -/mob/living/basic/bot/repairbot/turn_on() +/mob/living/basic/bot/repairbot/turn_on(mob/user) . = ..() if(!.) return diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm index 5167982defc4..de6253135fd2 100644 --- a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm +++ b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm @@ -52,7 +52,7 @@ GLOBAL_LIST_INIT(tendrils, list()) AddComponent(/datum/component/ai_target_timer) AddComponent(/datum/component/gps, "Eerie Signal") AddComponent(/datum/component/basic_mob_attack_telegraph, display_telegraph_overlay = FALSE, telegraph_duration = 0.4 SECONDS) - AddComponent(/datum/component/regenerator, regeneration_delay = 30 SECONDS, brute_per_second = 20) + AddComponent(/datum/component/regenerator, regeneration_delay = 30 SECONDS, brute_per_second = 20, outline_colour = COLOR_CULT_RED) add_traits(list(TRAIT_BACKSTAB_IMMUNE, TRAIT_IMMOBILIZED), INNATE_TRAIT) var/static/list/abilities = list( diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm index 86849f1ffd9f..5202159878b1 100644 --- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm +++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm @@ -175,12 +175,6 @@ // A note to future coders: do not replace this with an EMP because it will wreck malf AIs and everyone will hate you. /datum/action/cooldown/spell/aoe/revenant/malfunction/cast_on_thing_in_aoe(turf/victim, mob/living/basic/revenant/caster) - for(var/mob/living/simple_animal/bot/bot in victim) - if(!(bot.bot_cover_flags & BOT_COVER_EMAGGED)) - new /obj/effect/temp_visual/revenant(bot.loc) - bot.bot_cover_flags &= ~BOT_COVER_LOCKED - bot.bot_cover_flags |= BOT_COVER_MAINTS_OPEN - bot.emag_act(caster) for(var/mob/living/basic/bot/bot in victim) if(!(bot.bot_access_flags & BOT_COVER_EMAGGED)) new /obj/effect/temp_visual/revenant(bot.loc) diff --git a/code/modules/mob/living/basic/space_fauna/robot_customer.dm b/code/modules/mob/living/basic/space_fauna/robot_customer.dm index 7ffe1fadd6a5..1afe0b9c97b5 100644 --- a/code/modules/mob/living/basic/space_fauna/robot_customer.dm +++ b/code/modules/mob/living/basic/space_fauna/robot_customer.dm @@ -22,6 +22,7 @@ ai_controller = /datum/ai_controller/robot_customer damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1) + voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000" /// The clothes that we draw on this tourist. var/clothes_set = "amerifat_clothes" @@ -42,7 +43,8 @@ add_traits(list(TRAIT_NOMOBSWAP, TRAIT_NO_TELEPORT, TRAIT_STRONG_GRABBER), INNATE_TRAIT) // never suffer a bitch to fuck with you AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE) - + if(SStts.tts_enabled) + voice = pick(strings("robot_voices.json", "[customer_data.type]", "config")) ai_controller.set_blackboard_key(BB_CUSTOMER_CUSTOMERINFO, customer_info) ai_controller.set_blackboard_key(BB_CUSTOMER_ATTENDING_VENUE, attending_venue) ai_controller.set_blackboard_key(BB_CUSTOMER_PATIENCE, customer_info.total_patience) diff --git a/code/modules/mob/living/basic/stoats/stoat.dm b/code/modules/mob/living/basic/stoats/stoat.dm index 25d077dc54bb..0f5cfc82dd60 100644 --- a/code/modules/mob/living/basic/stoats/stoat.dm +++ b/code/modules/mob/living/basic/stoats/stoat.dm @@ -65,15 +65,15 @@ /mob/living/basic/stoat/kit = 100 // Placeholder until we get proper baby stoats ) AddComponent(\ - /datum/component/breed,\ - can_breed_with = typecacheof(list(/mob/living/basic/stoat)),\ - baby_paths = baby_paths,\ + /datum/component/breed,\ + can_breed_with = typecacheof(list(/mob/living/basic/stoat)),\ + baby_paths = baby_paths,\ ) /mob/living/basic/stoat/kit name = "\improper stoat kit" real_name = "stoat" - desc = "They're a stoat kit!" + desc = "An apex predator, but friend-shaped, and tiny..." icon_state = "kit_stoat" icon_living = "kit_stoat" icon_dead = "kit_stoat_dead" @@ -82,3 +82,15 @@ ai_controller = /datum/ai_controller/basic_controller/stoat/kit mob_size = MOB_SIZE_SMALL can_breed = FALSE + +/mob/living/basic/stoat/kit/Initialize(mapload) + . = ..() + AddComponent(\ + /datum/component/growth_and_differentiation,\ + growth_time = 20 MINUTES,\ + growth_path = /mob/living/basic/stoat,\ + growth_probability = 100,\ + lower_growth_value = 0.5,\ + upper_growth_value = 1,\ + signals_to_kill_on = list(COMSIG_MOB_CLIENT_LOGIN),\ + ) diff --git a/code/modules/mob/living/carbon/human/emote.dm b/code/modules/mob/living/carbon/human/emote.dm index 4a9f62ef24a3..dfa413a45ecc 100644 --- a/code/modules/mob/living/carbon/human/emote.dm +++ b/code/modules/mob/living/carbon/human/emote.dm @@ -63,7 +63,7 @@ message = "screeches!" message_mime = "screeches silently." emote_type = EMOTE_AUDIBLE | EMOTE_VISIBLE - specific_emote_audio_cooldown = 10 SECONDS + manual_specific_emote_audio_cooldown = 10 SECONDS vary = FALSE /datum/emote/living/carbon/human/screech/get_sound(mob/living/carbon/human/user) diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm index d33b6fd98782..3db4a61a06d9 100644 --- a/code/modules/mob/living/emote.dm +++ b/code/modules/mob/living/emote.dm @@ -303,7 +303,7 @@ message = "laughs." message_mime = "laughs silently!" emote_type = EMOTE_VISIBLE | EMOTE_AUDIBLE - specific_emote_audio_cooldown = 8 SECONDS + manual_specific_emote_audio_cooldown = 8 SECONDS vary = TRUE /datum/emote/living/laugh/can_run_emote(mob/living/user, status_check = TRUE , intentional, params) @@ -419,7 +419,9 @@ emote_type = EMOTE_VISIBLE | EMOTE_AUDIBLE mob_type_blacklist_typecache = list(/mob/living/brain) sound_wall_ignore = TRUE - specific_emote_audio_cooldown = 10 SECONDS + use_sound_tokens = TRUE + manual_specific_emote_audio_cooldown = 10 SECONDS + forced_specific_emote_audio_cooldown = 4 SECONDS vary = TRUE /datum/emote/living/scream/run_emote(mob/user, params, type_override, intentional = FALSE) diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index b4715780afdd..c43cef1d2d88 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -367,7 +367,12 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( message = deaf_message var/show_message_success = show_message(message, MSG_VISUAL, deaf_message, deaf_type, avoid_highlight) - return understood && show_message_success + if(show_message_success && understood) + return HEAR_HEARD | HEAR_UNDERSTOOD + else if (show_message_success && !understood) + return HEAR_HEARD + else + return FALSE if(speaker != src) if(!radio_freq) //These checks have to be separate, else people talking on the radio will make "You can't hear yourself!" appear when hearing people over the radio while deaf. @@ -388,9 +393,9 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( message = compose_message(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods) var/show_message_success = show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight) if(show_message_success && understood) - return TRUE + return HEAR_HEARD | HEAR_UNDERSTOOD else if (show_message_success && !understood) - return HEARD_BUT_DIDNT_UNDERSTAND + return HEAR_HEARD else return FALSE @@ -430,14 +435,6 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( listening |= player_mob var/tts_message_to_use = tts_message || message_raw - var/list/filter = list() - var/list/special_filter = list() - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - if(length(voice_filter) > 0) - filter += voice_filter - - if(length(tts_filter) > 0) - filter += tts_filter.Join(",") // this signal ignores whispers or language translations (only used by beetlejuice component) SEND_GLOBAL_SIGNAL(COMSIG_GLOB_LIVING_SAY_SPECIAL, src, message_raw) @@ -447,7 +444,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!") continue - if(listening_movable.Hear(src, message_language, message_raw, null, null, null, spans, message_mods, message_range)) + if(listening_movable.Hear(src, message_language, message_raw, null, null, null, spans, message_mods, message_range) & HEAR_HEARD) listened += listening_movable //speech bubble @@ -457,9 +454,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( if(M.client) if(!M.client.prefs.read_preference(/datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES))) speech_bubble_recipients.Add(M.client) - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, get_tts_voice(filter, special_filter), filter.Join(","), listened, message_range = message_range, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) - + do_tts_message(tts_message_to_use, message_language, message_mods, tts_filter, listened) var/image/say_popup = image('icons/mob/effects/talk.dmi', src, "[bubble_type][talk_icon_state]", FLY_LAYER) SET_PLANE_EXPLICIT(say_popup, ABOVE_GAME_PLANE, src) say_popup.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA @@ -467,7 +462,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( LAZYADD(update_on_z, say_popup) addtimer(CALLBACK(src, PROC_REF(clear_saypopup), say_popup), 3.5 SECONDS) -/mob/living/proc/get_tts_voice(list/filter, list/special_filter) +/mob/living/get_tts_voice(list/filter, list/special_filter) . = voice var/obj/item/clothing/mask/mask = get_item_by_slot(ITEM_SLOT_MASK) if(!istype(mask) || mask.up) diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index aa960b3444a4..0849b20eed9a 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -541,13 +541,8 @@ var/mob/living/bot = bot_ref?.resolve() if(!bot) return - var/summon_success - if(isbasicbot(bot)) - var/mob/living/basic/bot/basic_bot = bot - summon_success = basic_bot.summon_bot(src, waypoint, grant_all_access = TRUE) - else - var/mob/living/simple_animal/bot/simple_bot = bot - summon_success = simple_bot.call_bot(src, waypoint) + var/mob/living/basic/bot/basic_bot = bot + var/summon_success = basic_bot.summon_bot(src, waypoint, grant_all_access = TRUE) var/chat_message = summon_success ? "Sending command to bot..." : "Interface error. Unit is already in use." to_chat(src, span_notice("[chat_message]")) diff --git a/code/modules/mob/living/silicon/ai/ai_say.dm b/code/modules/mob/living/silicon/ai/ai_say.dm index d57afeb8876c..7895dd65882c 100644 --- a/code/modules/mob/living/silicon/ai/ai_say.dm +++ b/code/modules/mob/living/silicon/ai/ai_say.dm @@ -30,32 +30,17 @@ return FALSE . = ..() if(.) - var/list/filter = list() - var/list/special_filter = list() - if(length(voice_filter) > 0) - filter += voice_filter - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) + do_tts_message(message, language, message_mods, list(), list()) return . if(message_mods[MODE_HEADSET]) if(radio) radio.talk_into(src, message, , spans, language, message_mods) - var/list/filter = list() - var/list/special_filter = list() - if(length(voice_filter) > 0) - filter += voice_filter - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) + do_tts_message(message, language, message_mods, list(), list()) return NOPASS else if(message_mods[RADIO_EXTENSION] in GLOB.default_radio_channels) if(radio) radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) - var/list/filter = list() - var/list/special_filter = list() - if(length(voice_filter) > 0) - filter += voice_filter - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) + do_tts_message(message, language, message_mods, list(), list()) return NOPASS return FALSE diff --git a/code/modules/mob/living/silicon/ai/robot_control.dm b/code/modules/mob/living/silicon/ai/robot_control.dm index 94c99369b6c8..1884804afe4e 100644 --- a/code/modules/mob/living/silicon/ai/robot_control.dm +++ b/code/modules/mob/living/silicon/ai/robot_control.dm @@ -35,37 +35,22 @@ var/turf/ai_current_turf = get_turf(owner) data["robots"] = list() - for(var/mob/living/our_bot as anything in GLOB.bots_list) + for(var/mob/living/basic/bot/our_bot as anything in GLOB.bots_list) if(!isbot(our_bot) || !is_valid_z_level(ai_current_turf, get_turf(our_bot))) continue - if(isbasicbot(our_bot)) - var/mob/living/basic/bot/basic_bot = our_bot - if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) - continue - var/list/basic_bot_data = list( - name = basic_bot.name, - model = basic_bot.bot_type, - mode = basic_bot.mode, - hacked = !!(basic_bot.bot_access_flags & BOT_COVER_HACKED), - location = get_area_name(basic_bot, TRUE), - ref = REF(basic_bot), - ) - data["robots"] += list(basic_bot_data) - continue - - var/mob/living/simple_animal/bot/simple_bot = our_bot - if(!(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) + var/mob/living/basic/bot/basic_bot = our_bot + if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) continue - var/list/simple_bot_data = list( - name = simple_bot.name, - model = simple_bot.bot_type, - mode = simple_bot.get_mode(), - hacked = !!(simple_bot.bot_cover_flags & BOT_COVER_HACKED), - location = get_area_name(simple_bot, TRUE), - ref = REF(simple_bot), + var/list/basic_bot_data = list( + name = basic_bot.name, + model = basic_bot.bot_type, + mode = basic_bot.mode, + hacked = !!(basic_bot.bot_access_flags & BOT_COVER_HACKED), + location = get_area_name(basic_bot, TRUE), + ref = REF(basic_bot), ) - data["robots"] += list(simple_bot_data) + data["robots"] += list(basic_bot_data) return data @@ -84,28 +69,19 @@ switch(action) if("callbot") //Command a bot to move to a selected location. - if(isbasicbot(bot)) - var/mob/living/basic/bot/basic_bot = bot - if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) - return - else - var/mob/living/simple_animal/bot/simple_bot = bot - if(!(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) - return + var/mob/living/basic/bot/basic_bot = bot + if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) + return owner.bot_ref = WEAKREF(bot) owner.setting_waypoint = TRUE to_chat(our_user, span_notice("Set your waypoint by clicking on a valid location free of obstructions.")) if("interface") //Remotely connect to a bot! owner.bot_ref = WEAKREF(bot) - if(isbasicbot(bot)) - var/mob/living/basic/bot/basic_bot = bot - if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) - return - else - var/mob/living/basic/bot/simple_bot = bot - if(!(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) - return + var/mob/living/basic/bot/basic_bot = bot + if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) + return + bot.attack_ai(our_user) return TRUE diff --git a/code/modules/mob/living/silicon/robot/inventory.dm b/code/modules/mob/living/silicon/robot/inventory.dm index e5e3e6f867d2..a2d30cdac46a 100644 --- a/code/modules/mob/living/silicon/robot/inventory.dm +++ b/code/modules/mob/living/silicon/robot/inventory.dm @@ -257,7 +257,6 @@ if(module && module_active != held_items[module_num]) module.icon_state = "[module.base_icon_state] +a" module_active = held_items[module_num] - SEND_SIGNAL(module_active, COMSIG_SILICON_MODULE_ACTIVATION, TRUE) return TRUE /** @@ -269,8 +268,6 @@ var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_HAND_SLOT(module_num)] if(module) module.icon_state = module.base_icon_state - if(module_active) - SEND_SIGNAL(module_active, COMSIG_SILICON_MODULE_ACTIVATION, FALSE) module_active = null return TRUE diff --git a/code/modules/mob/living/silicon/robot/robot_defines.dm b/code/modules/mob/living/silicon/robot/robot_defines.dm index c4cf75317d9b..267c01db5fde 100644 --- a/code/modules/mob/living/silicon/robot/robot_defines.dm +++ b/code/modules/mob/living/silicon/robot/robot_defines.dm @@ -13,7 +13,6 @@ health = 100 bubble_icon = "robot" designation = "Default" //used for displaying the prefix & getting the current model of cyborg - has_limbs = TRUE hud_type = /datum/hud/robot unique_name = TRUE mouse_drop_zone = TRUE diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm index 1c755a020781..d60e9d91600a 100644 --- a/code/modules/mob/living/silicon/robot/robot_model.dm +++ b/code/modules/mob/living/silicon/robot/robot_model.dm @@ -389,6 +389,7 @@ /obj/item/construction/rcd/borg, /obj/item/pipe_dispenser, /obj/item/extinguisher, + /obj/item/weldingtool/largetank/cyborg, /obj/item/borg/cyborg_omnitool/engineering, /obj/item/borg/cyborg_omnitool/engineering, /obj/item/t_scanner, @@ -412,6 +413,7 @@ model_select_icon = "engineer" model_traits = list(TRAIT_NEGATES_GRAVITY) hat_offset = list("north" = list(0, -4), "south" = list(0, -4), "east" = list(4, -4), "west" = list(-4, -4)) + ///Weakref to the night vision action var/datum/weakref/night_vision_ref /datum/action/cooldown/borg_meson @@ -428,10 +430,12 @@ borg.update_sight() /obj/item/robot_model/engineering/be_transformed_to(obj/item/robot_model/old_model, forced = FALSE) - var/datum/action/cooldown/borg_meson/night_vision = new(loc) . = ..() if(!.) return + + //Grant night vision action + var/datum/action/cooldown/borg_meson/night_vision = new(loc) night_vision.Grant(loc) night_vision_ref = WEAKREF(night_vision) @@ -970,6 +974,7 @@ /obj/item/pipe_dispenser, /obj/item/restraints/handcuffs/cable/zipties, /obj/item/extinguisher, + /obj/item/weldingtool/largetank/cyborg, /obj/item/analyzer, /obj/item/borg/cyborg_omnitool/engineering, /obj/item/borg/cyborg_omnitool/engineering, diff --git a/code/modules/mob/living/silicon/silicon_say.dm b/code/modules/mob/living/silicon/silicon_say.dm index 6c15289ce49c..e7160433f1aa 100644 --- a/code/modules/mob/living/silicon/silicon_say.dm +++ b/code/modules/mob/living/silicon/silicon_say.dm @@ -87,32 +87,16 @@ /mob/living/silicon/radio(message, list/message_mods = list(), list/spans, language) . = ..() if(.) - var/list/filter = list() - var/list/special_filter = list() - if(length(voice_filter) > 0) - filter += voice_filter - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) + do_tts_message(message, language, message_mods, list(), list()) return if(message_mods[MODE_HEADSET]) if(radio) - radio.talk_into(src, message, , spans, language, message_mods) - var/list/filter = list() - var/list/special_filter = list() - if(length(voice_filter) > 0) - filter += voice_filter - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) + radio.talk_into(src, message, null, spans, language, message_mods) + do_tts_message(message, language, message_mods, list(), list()) return NOPASS else if(message_mods[RADIO_EXTENSION] in GLOB.default_radio_channels) if(radio) radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) - var/list/filter = list() - var/list/special_filter = list() - if(length(voice_filter) > 0) - filter += voice_filter - if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE)) - INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER]) + do_tts_message(message, language, message_mods, list(), list()) return NOPASS - return FALSE diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm deleted file mode 100644 index 86f50e6246ad..000000000000 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ /dev/null @@ -1,1231 +0,0 @@ -// AI (i.e. game AI, not the AI player) controlled bots -/mob/living/simple_animal/bot - abstract_type = /mob/living/simple_animal/bot - icon = 'icons/mob/silicon/aibots.dmi' - layer = MOB_LAYER - gender = NEUTER - mob_biotypes = MOB_ROBOTIC - stop_automated_movement = TRUE - wander = FALSE - damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, STAMINA = 0, OXY = 0) - atmos_requirements = null - unsuitable_atmos_damage = 0 - hud_possible = list(DIAG_STAT_HUD, DIAG_BOT_HUD, DIAG_HUD, DIAG_BATT_HUD, DIAG_PATH_HUD = HUD_LIST_LIST) - maxbodytemp = INFINITY - minbodytemp = 0 - sentience_type = SENTIENCE_ARTIFICIAL - status_flags = NONE //no default canpush - pass_flags = PASSFLAPS - verb_say = "states" - verb_ask = "queries" - verb_exclaim = "declares" - verb_yell = "alarms" - initial_language_holder = /datum/language_holder/synthetic - bubble_icon = "machine" - speech_span = SPAN_ROBOT - faction = list(FACTION_NEUTRAL, FACTION_SILICON, FACTION_TURRET) - light_system = OVERLAY_LIGHT - light_range = 3 - light_power = 0.6 - del_on_death = TRUE - req_one_access = list(ACCESS_ROBOTICS) - interaction_flags_click = ALLOW_SILICON_REACH - - ///Cooldown between salutations for commissioned bots - COOLDOWN_DECLARE(next_salute_check) - - ///The Robot arm attached to this robot - has a 50% chance to drop on death. - var/robot_arm = /obj/item/bodypart/arm/right/robot - ///The inserted (if any) pAI in this bot. - var/obj/item/pai_card/paicard - ///The type of bot it is, for radio control. - var/bot_type = NONE - - ///Additonal access given to player-controlled bots. - var/list/player_access = list() - ///All initial access this bot started with. - var/list/prev_access = list() - - ///Bot-related mode flags on the Bot indicating how they will act. BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION - var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION - - ///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging. BOT_COVER_MAINTS_OPEN | BOT_COVER_LOCKED | BOT_COVER_EMAGGED | BOT_COVER_HACKED - var/bot_cover_flags = BOT_COVER_LOCKED - - ///Small name of what the bot gets messed with when getting hacked/emagged. - var/hackables = "system circuits" - ///Used by some bots for tracking failures to reach their target. - var/frustration = 0 - ///The speed at which the bot moves, or the number of times it moves per process() tick. - var/base_speed = 2 - ///The end point of a bot's path, or the target location. - var/turf/ai_waypoint - ///The bot is on a custom set path. - var/pathset = FALSE - ///List of turfs through which a bot 'steps' to reach the waypoint, associated with the path image, if there is one. - var/list/path = list() - ///List of unreachable targets for an ignore-list enabled bot to ignore. - var/list/ignore_list = list() - ///Standardizes the vars that indicate the bot is busy with its function. - var/mode = BOT_IDLE - ///Number of times the bot tried and failed to move. - var/tries = 0 - ///Links a bot to the AI calling it. - var/mob/living/silicon/ai/calling_ai - ///The bot's radio, for speaking to people. - var/obj/item/radio/internal_radio - ///which channels can the bot listen to - var/radio_key = null - ///The bot's default radio channel - var/radio_channel = RADIO_CHANNEL_COMMON - ///Turf a bot is summoned to navitage towards. - var/turf/patrol_target - ///Turf of a user summoning a bot towards their location. - var/turf/summon_target - ///Pending new destination (waiting for beacon response) - var/new_destination - ///Destination description tag - var/destination - ///The next destination in the patrol route - var/next_destination - - /// the nearest beacon's tag - var/nearest_beacon - ///The nearest beacon's location - var/turf/nearest_beacon_loc - - ///The type of data HUD the bot uses. Diagnostic by default. - var/data_hud_type = TRAIT_DIAGNOSTIC_HUD - var/datum/atom_hud/data/bot_path/private/path_hud - var/path_image_icon = 'icons/mob/silicon/aibots.dmi' - var/path_image_icon_state = "path_indicator" - var/path_image_color = COLOR_WHITE - var/reset_access_timer_id - var/ignorelistcleanuptimer = 1 // This ticks up every automated action, at 300 we clean the ignore list - - /// If true we will allow ghosts to control this mob - var/can_be_possessed = FALSE - /// If true we will offer this - COOLDOWN_DECLARE(offer_ghosts_cooldown) - /// Message to display upon possession - var/possessed_message = "You're a generic bot. How did one of these even get made?" - /// List of strings to sound effects corresponding to automated messages the bot can play - var/list/automated_announcements - /// Action we use to say voice lines out loud, also we just pass anything we try to say through here just in case it plays a voice line - var/datum/action/cooldown/bot_announcement/pa_system - // The faction of the bot before it inherited the pai's faction - var/list/original_faction - // The allies of the bot before it inherited the pai's faction - var/list/original_allies - - ///Innate access uses an internal ID card. - var/obj/item/card/id/access_card = null - -/mob/living/simple_animal/bot/proc/get_mode() - if(client) //Player bots do not have modes, thus the override. Also an easy way for PDA users/AI to know when a bot is a player. - return paicard ? "pAI Controlled" : "Autonomous" - if(!(bot_mode_flags & BOT_MODE_ON)) - return span_bad("Inactive") - return span_average("[mode]") - -/** - * Returns a status string about the bot's current status, if it's moving, manually controlled, or idle. - */ -/mob/living/simple_animal/bot/proc/get_mode_ui() - if(client) //Player bots do not have modes, thus the override. Also an easy way for PDA users/AI to know when a bot is a player. - return paicard ? "pAI Controlled" : "Autonomous" - if(!(bot_mode_flags & BOT_MODE_ON)) - return "Inactive" - return "[mode]" - -/** - * Returns a string of flavor text for emagged bots as defined by policy. - */ -/mob/living/simple_animal/bot/proc/get_emagged_message() - return get_policy(ROLE_EMAGGED_BOT) || "You are a malfunctioning bot! Disrupt everyone and cause chaos!" - -/mob/living/simple_animal/bot/proc/turn_on() - if(stat) - return FALSE - bot_mode_flags |= BOT_MODE_ON - remove_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), POWER_LACK_TRAIT) - set_light_on(bot_mode_flags & BOT_MODE_ON ? TRUE : FALSE) - update_appearance() - balloon_alert(src, "turned on") - diag_hud_set_botstat() - return TRUE - -/mob/living/simple_animal/bot/proc/turn_off() - bot_mode_flags &= ~BOT_MODE_ON - add_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), POWER_LACK_TRAIT) - set_light_on(bot_mode_flags & BOT_MODE_ON ? TRUE : FALSE) - bot_reset() //Resets an AI's call, should it exist. - balloon_alert(src, "turned off") - update_appearance() - -/mob/living/simple_animal/bot/proc/get_bot_flag(checked_mode, checked_flag) - if(checked_mode & checked_flag) - return TRUE - return FALSE - -/mob/living/simple_animal/bot/Initialize(mapload) - . = ..() - GLOB.bots_list += src - add_traits(list(TRAIT_SILICON_ACCESS, TRAIT_REAGENT_SCANNER, TRAIT_UNOBSERVANT), INNATE_TRAIT) - LoadComponent(/datum/component/bloodysoles/bot) - - path_hud = new /datum/atom_hud/data/bot_path/private() - for(var/hud in path_hud.hud_icons) // You get to see your own path - set_hud_image_active(hud, exclusive_hud = path_hud) - - // Give bots a fancy new ID card that can hold any access. - access_card = new /obj/item/card/id/advanced/simple_bot(src) - // This access is so bots can be immediately set to patrol and leave Robotics, instead of having to be let out first. - access_card.set_access(list(ACCESS_ROBOTICS)) - internal_radio = new /obj/item/radio(src) - if(radio_key) - internal_radio.keyslot = new radio_key - internal_radio.subspace_transmission = TRUE - internal_radio.canhear_range = 0 // anything greater will have the bot broadcast the channel as if it were saying it out loud. - internal_radio.recalculateChannels() - - //Adds bot to the diagnostic HUD system - prepare_huds() - var/datum/atom_hud/data/diagnostic/diag_hud = GLOB.huds[DATA_HUD_DIAGNOSTIC] - diag_hud.add_atom_to_hud(src) - diag_hud_set_bothealth() - diag_hud_set_botstat() - diag_hud_set_botmode() - - //If a bot has its own HUD (for player bots), provide it. - if(!isnull(data_hud_type)) - ADD_TRAIT(src, data_hud_type, INNATE_TRAIT) - if(path_hud) - path_hud.add_atom_to_hud(src) - path_hud.show_to(src) - - if(mapload && is_station_level(z) && bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT && bot_mode_flags & BOT_MODE_ROUNDSTART_POSSESSION) - enable_possession(mapload = mapload) - - pa_system = new(src, automated_announcements = automated_announcements) - pa_system.Grant(src) - RegisterSignal(src, COMSIG_MOB_TRIED_ACCESS, PROC_REF(attempt_access)) - ADD_TRAIT(src, TRAIT_SILICON_EMOTES_ALLOWED, INNATE_TRAIT) - -/mob/living/simple_animal/bot/Destroy() - GLOB.bots_list -= src - QDEL_NULL(paicard) - QDEL_NULL(pa_system) - QDEL_NULL(internal_radio) - QDEL_NULL(access_card) - QDEL_NULL(path_hud) - return ..() - -/// Allows this bot to be controlled by a ghost, who will become its mind -/mob/living/simple_animal/bot/proc/enable_possession(user, mapload = FALSE) - if (paicard) - balloon_alert(user, "already sapient!") - return - can_be_possessed = TRUE - var/can_announce = !mapload && COOLDOWN_FINISHED(src, offer_ghosts_cooldown) - AddComponent( - /datum/component/ghost_direct_control, \ - ban_type = ROLE_BOT, \ - poll_candidates = can_announce, \ - poll_ignore_key = POLL_IGNORE_BOTS, \ - assumed_control_message = (bot_cover_flags & BOT_COVER_EMAGGED) ? get_emagged_message() : possessed_message, \ - extra_control_checks = CALLBACK(src, PROC_REF(check_possession)), \ - after_assumed_control = CALLBACK(src, PROC_REF(post_possession)), \ - ) - if (can_announce) - COOLDOWN_START(src, offer_ghosts_cooldown, 30 SECONDS) - - if (user) - log_silicon("[key_name(user)] enabled sapience for [src] ([initial(src.name)])") // Not technically a silicon but who is counting - -/// Disables this bot from being possessed by ghosts -/mob/living/simple_animal/bot/proc/disable_possession(mob/user) - if (user) - log_silicon("[key_name(user)] disabled sapience for [src] ([initial(src.name)])") - can_be_possessed = FALSE - qdel(GetComponent(/datum/component/ghost_direct_control)) - if (isnull(key)) - return - if (user) - log_combat(user, src, "ejected [key_name(src)] from control of [src] ([initial(src.name)]).") - to_chat(src, span_warning("You feel yourself fade as your personality matrix is reset!")) - ghostize(can_reenter_corpse = FALSE) - playsound(src, 'sound/machines/ping.ogg', 30, TRUE) - speak("Personality matrix reset!") - key = null - -/// Returns true if this mob can be controlled -/mob/living/simple_animal/bot/proc/check_possession(mob/potential_possessor) - if (!can_be_possessed) - to_chat(potential_possessor, span_warning("The bot's personality download has been disabled!")) - return can_be_possessed - -/// Fired after something takes control of this mob -/mob/living/simple_animal/bot/proc/post_possession() - playsound(src, 'sound/machines/ping.ogg', 30, TRUE) - speak("New personality installed successfully!") - rename(src) - -/// Allows renaming the bot to something else -/mob/living/simple_animal/bot/proc/rename(mob/user) - var/new_name = sanitize_name( - reject_bad_text(tgui_input_text( - user = user, - message = "This machine is designated [real_name]. Would you like to update its registration?", - title = "Name change", - default = real_name, - max_length = MAX_NAME_LEN, - )), - allow_numbers = TRUE - ) - if (isnull(new_name) || QDELETED(src)) - return - if (key && user != src) - var/accepted = tgui_alert( - src, - message = "Do you wish to be renamed to [new_name]?", - title = "Name change", - buttons = list("Yes", "No"), - ) - if (accepted != "Yes" || QDELETED(src)) - return - fully_replace_character_name(real_name, new_name) - -/mob/living/simple_animal/bot/allowed(mob/living/user) - if(!(bot_cover_flags & BOT_COVER_LOCKED)) // Unlocked. - return TRUE - return ..() - -/mob/living/simple_animal/bot/bee_friendly() - return TRUE - -/mob/living/simple_animal/bot/death(gibbed) - if(paicard) - ejectpai() - explode() - return ..() - -/mob/living/simple_animal/bot/proc/explode() - visible_message(span_boldnotice("[src] blows apart!")) - do_sparks(3, TRUE, src) - var/atom/location_destroyed = drop_location() - if(prob(50)) - drop_part(robot_arm, location_destroyed) - -/mob/living/simple_animal/bot/emag_act(mob/user, obj/item/card/emag/emag_card) - . = ..() - if(bot_cover_flags & BOT_COVER_LOCKED) //First emag application unlocks the bot's interface. Apply a screwdriver to use the emag again. - bot_cover_flags &= ~BOT_COVER_LOCKED - balloon_alert(user, "cover unlocked") - return TRUE - if(!(bot_cover_flags & BOT_COVER_LOCKED) && bot_cover_flags & BOT_COVER_MAINTS_OPEN) //Bot panel is unlocked by ID or emag, and the panel is screwed open. Ready for emagging. - bot_cover_flags |= BOT_COVER_EMAGGED - bot_cover_flags &= ~BOT_COVER_LOCKED //Manually emagging the bot locks out the panel. - bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED //Manually emagging the bot also locks the AI from controlling it. - bot_reset() - turn_on() //The bot automatically turns on when emagged, unless recently hit with EMP. - to_chat(src, span_userdanger("(#$*#$^^( OVERRIDE DETECTED")) - to_chat(src, span_boldnotice(get_emagged_message())) - if(user) - log_combat(user, src, "emagged") - return TRUE - else //Bot is unlocked, but the maint panel has not been opened with a screwdriver (or through the UI) yet. - balloon_alert(user, "open maintenance panel first!") - return FALSE - -/mob/living/simple_animal/bot/examine(mob/user) - . = ..() - if(health < maxHealth) - if(health > maxHealth/3) - . += "[src]'s parts look loose." - else - . += "[src]'s parts look very loose!" - else - . += "[src] is in pristine condition." - . += span_notice("[p_Their()] maintenance panel is [bot_cover_flags & BOT_COVER_MAINTS_OPEN ? "open" : "closed"].") - . += span_info("You can use a screwdriver to [bot_cover_flags & BOT_COVER_MAINTS_OPEN ? "close" : "open"] it.") - if(bot_cover_flags & BOT_COVER_MAINTS_OPEN) - . += span_notice("[p_Their()] control panel is [bot_cover_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"].") - var/is_sillycone = HAS_SILICON_ACCESS(user) - if(!(bot_cover_flags & BOT_COVER_EMAGGED) && (is_sillycone || user.Adjacent(src))) - . += span_info("Alt-click [is_sillycone ? "" : "or use your ID on "]it to [bot_cover_flags & BOT_COVER_LOCKED ? "un" : ""]lock [p_their()] control panel.") - if(paicard) - . += span_notice("[p_They()] has a pAI device installed.") - if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN)) - . += span_info("You can use a hemostat to remove it.") - if(access_card) - . += "There appears to be [icon2html(access_card, user)] \a [access_card] pinned to [p_them()]." - -/mob/living/simple_animal/bot/adjustHealth(amount, updating_health = TRUE, forced = FALSE) - if(amount > 0 && prob(10)) - new /obj/effect/decal/cleanable/blood/oil(loc) - return ..() - -/mob/living/simple_animal/bot/updatehealth() - ..() - diag_hud_set_bothealth() - -/mob/living/simple_animal/bot/med_hud_set_health() - return //we use a different hud - -/mob/living/simple_animal/bot/med_hud_set_status() - return //we use a different hud - -/mob/living/simple_animal/bot/handle_automated_action() //Master process which handles code common across most bots. - diag_hud_set_botmode() - - if (ignorelistcleanuptimer % 300 == 0) // Every 300 actions, clean up the ignore list from old junk - for(var/ref in ignore_list) - var/atom/referredatom = locate(ref) - if (!referredatom || !istype(referredatom) || QDELETED(referredatom)) - ignore_list -= ref - ignorelistcleanuptimer = 1 - else - ignorelistcleanuptimer++ - - if(!(bot_mode_flags & BOT_MODE_ON) || client) - return FALSE - - if(HAS_TRAIT(src, TRAIT_COMMISSIONED) && COOLDOWN_FINISHED(src, next_salute_check)) - COOLDOWN_START(src, next_salute_check, BOT_COMMISSIONED_SALUTE_DELAY) - for(var/mob/living/simple_animal/bot/nearby_bot in view(5, src)) - if(!HAS_TRAIT(nearby_bot, TRAIT_COMMISSIONED) && nearby_bot.bot_mode_flags & BOT_MODE_ON) - manual_emote("performs an elaborate salute for [nearby_bot]!") - break - - switch(mode) //High-priority overrides are processed first. Bots can do nothing else while under direct command. - if(BOT_RESPONDING) //Called by the AI. - call_mode() - return FALSE - if(BOT_SUMMON) //Called to a location - summon_step() - return FALSE - return TRUE //Successful completion. Used to prevent child process() continuing if this one is ended early. - - -/mob/living/simple_animal/bot/attack_hand(mob/living/carbon/human/user, list/modifiers) - if(!user.combat_mode) - ui_interact(user) - else - return ..() - -/mob/living/simple_animal/bot/attack_ai(mob/user) - if(!topic_denied(user)) - ui_interact(user) - else - to_chat(user, span_warning("[src]'s interface is not responding!")) - -/mob/living/simple_animal/bot/ui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "SimpleBot", name) - ui.open() - -/mob/living/simple_animal/bot/click_alt(mob/user) - unlock_with_id(user) - return CLICK_ACTION_SUCCESS - -/mob/living/simple_animal/bot/proc/unlock_with_id(mob/user) - if(bot_cover_flags & BOT_COVER_EMAGGED) - to_chat(user, span_danger("ERROR")) - return - if(bot_cover_flags & BOT_COVER_MAINTS_OPEN) - to_chat(user, span_warning("Please close the access panel before [bot_cover_flags & BOT_COVER_LOCKED ? "un" : ""]locking it.")) - return - if(!allowed(user)) - to_chat(user, span_warning("Access denied.")) - return - bot_cover_flags ^= BOT_COVER_LOCKED - to_chat(user, span_notice("Controls are now [bot_cover_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"].")) - return TRUE - -/mob/living/simple_animal/bot/screwdriver_act(mob/living/user, obj/item/tool) - if(bot_cover_flags & BOT_COVER_LOCKED) - to_chat(user, span_warning("The maintenance panel is locked!")) - return ITEM_INTERACT_SUCCESS - - tool.play_tool_sound(src) - bot_cover_flags ^= BOT_COVER_MAINTS_OPEN - to_chat(user, span_notice("The maintenance panel is now [bot_cover_flags & BOT_COVER_MAINTS_OPEN ? "opened" : "closed"].")) - return ITEM_INTERACT_SUCCESS - -/mob/living/simple_animal/bot/welder_act(mob/living/user, obj/item/tool) - user.changeNext_move(CLICK_CD_MELEE) - if(user.combat_mode) - return FALSE - - if(health >= maxHealth) - to_chat(user, span_warning("[src] does not need a repair!")) - return ITEM_INTERACT_SUCCESS - if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN)) - to_chat(user, span_warning("Unable to repair with the maintenance panel closed!")) - return ITEM_INTERACT_SUCCESS - - if(tool.use_tool(src, user, 0 SECONDS, volume=40)) - adjustHealth(-10) - user.visible_message(span_notice("[user] repairs [src]!"),span_notice("You repair [src].")) - return ITEM_INTERACT_SUCCESS - -/mob/living/simple_animal/bot/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) - if(attacking_item.GetID()) - unlock_with_id(user) - return - if(istype(attacking_item, /obj/item/pai_card)) - insertpai(user, attacking_item) - return - if(attacking_item.tool_behaviour == TOOL_HEMOSTAT && paicard) - if(bot_cover_flags & BOT_COVER_MAINTS_OPEN) - balloon_alert(user, "open the access panel!") - else - balloon_alert(user, "removing pAI...") - if(!do_after(user, 3 SECONDS, target = src) || !paicard) - return - user.visible_message(span_notice("[user] uses [attacking_item] to pull [paicard] out of [initial(src.name)]!"),span_notice("You pull [paicard] out of [initial(src.name)] with [attacking_item].")) - ejectpai(user) - return - return ..() - -/mob/living/simple_animal/bot/attack_effects(damage_done, hit_zone, armor_block, obj/item/attacking_item, mob/living/attacker) - if(damage_done > 0 && attacking_item.damtype != STAMINA && stat != DEAD) - do_sparks(5, TRUE, src) - . = TRUE - return ..() || . - -/mob/living/simple_animal/bot/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE) - . = ..() - if(prob(25) || . != BULLET_ACT_HIT) - return - if(hitting_projectile.damage_type != BRUTE && hitting_projectile.damage_type != BURN) - return - if(!hitting_projectile.is_hostile_projectile() || hitting_projectile.damage <= 0) - return - do_sparks(5, TRUE, src) - -/mob/living/simple_animal/bot/emp_act(severity) - . = ..() - if(. & EMP_PROTECT_SELF) - return - var/was_on = bot_mode_flags & BOT_MODE_ON ? TRUE : FALSE - stat |= EMPED - new /obj/effect/temp_visual/emp(loc) - if(paicard) - paicard.emp_act(severity) - src.visible_message(span_notice("[paicard] is flies out of [initial(src.name)]!"), span_warning("You are forcefully ejected from [initial(src.name)]!")) - ejectpai() - - if (QDELETED(src)) - return - - if(bot_mode_flags & BOT_MODE_ON) - turn_off() - addtimer(CALLBACK(src, PROC_REF(emp_reset), was_on), severity * 30 SECONDS) - if(!prob(70/severity)) - return - if (!length(GLOB.uncommon_roundstart_languages)) - return - remove_all_languages(source = LANGUAGE_EMP) - grant_random_uncommon_language(source = LANGUAGE_EMP) - -/mob/living/simple_animal/bot/proc/emp_reset(was_on) - stat &= ~EMPED - if(was_on) - turn_on() - -/** - * Pass a message to have the bot say() it, passing through our announcement action to potentially also play a sound. - * Optionally pass a frequency to say it on the radio. - */ -/mob/living/simple_animal/bot/proc/speak(message, channel) - if(!message) - return - pa_system.announce(message, channel) - -/mob/living/simple_animal/bot/radio(message, list/message_mods = list(), list/spans, language) - . = ..() - if(.) - return - - if(message_mods[MODE_HEADSET]) - internal_radio.talk_into(src, message, , spans, language, message_mods) - return REDUCE_RANGE - else if(message_mods[RADIO_EXTENSION] == MODE_DEPARTMENT) - internal_radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) - return REDUCE_RANGE - else if(message_mods[RADIO_EXTENSION] in GLOB.default_radio_channels) - internal_radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) - return REDUCE_RANGE - -/mob/living/simple_animal/bot/proc/drop_part(obj/item/drop_item, dropzone) - var/obj/item/item_to_drop - if(ispath(drop_item)) - item_to_drop = new drop_item(dropzone) - else - item_to_drop = drop_item - item_to_drop.forceMove(dropzone) - - if(istype(item_to_drop, /obj/item/stock_parts/power_store/cell)) - var/obj/item/stock_parts/power_store/cell/dropped_cell = item_to_drop - dropped_cell.charge = 0 - - else if(istype(item_to_drop, /obj/item/storage)) - var/obj/item/storage/storage_to_drop = item_to_drop - storage_to_drop.contents = list() - - else if(istype(item_to_drop, /obj/item/gun/energy)) - var/obj/item/gun/energy/dropped_gun = item_to_drop - dropped_gun.cell.charge = 0 - dropped_gun.update_appearance() - -//Generalized behavior code, override where needed! - -GLOBAL_LIST_EMPTY(scan_typecaches) -/** - * Attempt to scan tiles near [src], first by checking adjacent, then if a target is still not found, nearby. - * - * scan_types - list (of typepaths) that nearby tiles are being scanned for. - * old_target - what has already been scanned, and will early return at checkscan. - * scan_range - how far away from [src] will be scanned, if nothing is found directly adjacent. - */ -/mob/living/simple_animal/bot/proc/scan(list/scan_types, old_target, scan_range = DEFAULT_SCAN_RANGE) - var/key = scan_types.Join(",") - var/list/scan_cache = GLOB.scan_typecaches[key] - if(!scan_cache) - scan_cache = typecacheof(scan_types) - GLOB.scan_typecaches[key] = scan_cache - if(!get_turf(src)) - return - // Nicer behavior, ensures we don't conflict with other bots quite so often - var/list/adjacent = list() - for(var/turf/to_walk in view(1, src)) - adjacent += to_walk - - adjacent = shuffle(adjacent) - - var/list/turfs_to_walk = list() - for(var/turf/victim in view(scan_range, src)) - turfs_to_walk += victim - - turfs_to_walk = turfs_to_walk - adjacent - // Now we prepend adjacent since we want to run those first - turfs_to_walk = adjacent + turfs_to_walk - - for(var/turf/scanned as anything in turfs_to_walk) - // Check bot is inlined here to save cpu time - //Is there another bot there? Then let's just skip it so we dont all atack on top of eachother. - var/bot_found = FALSE - for(var/mob/living/simple_animal/bot/buddy in scanned.contents) - if(istype(buddy, type) && (buddy != src)) - bot_found = TRUE - break - if(bot_found) - continue - - for(var/atom/thing as anything in scanned) - if(!scan_cache[thing.type]) //Check that the thing we found is the type we want! - continue //If not, keep searching! - if(thing == old_target || (REF(thing) in ignore_list)) //Filter for blacklisted elements, usually unreachable or previously processed oness - continue - - var/scan_result = process_scan(thing) //Some bots may require additional processing when a result is selected. - if(!isnull(scan_result)) - return scan_result - -//When the scan finds a target, run bot specific processing to select it for the next step. Empty by default. -/mob/living/simple_animal/bot/proc/process_scan(scan_target) - return scan_target - -/mob/living/simple_animal/bot/proc/check_bot(targ) - var/turf/target_turf = get_turf(targ) - if(!target_turf) - return FALSE - for(var/mob/living/simple_animal/bot/buddy in target_turf.contents) - if(istype(buddy, type) && (buddy != src)) - return TRUE - return FALSE - -/mob/living/simple_animal/bot/proc/add_to_ignore(subject) - if(ignore_list.len < 50) //This will help keep track of them, so the bot is always trying to reach a blocked spot. - ignore_list += REF(subject) - else //If the list is full, insert newest, delete oldest. - ignore_list.Cut(1,2) - ignore_list += REF(subject) - -/* -Movement proc for stepping a bot through a path generated through A-star. -Pass a positive integer as an argument to override a bot's default speed. -*/ -/mob/living/simple_animal/bot/proc/bot_move(dest, move_speed) - if(!dest || !path || path.len == 0) //A-star failed or a path/destination was not set. - set_path(null) - return FALSE - dest = get_turf(dest) //We must always compare turfs, so get the turf of the dest var if dest was originally something else. - var/turf/last_node = get_turf(path[path.len]) //This is the turf at the end of the path, it should be equal to dest. - if(get_turf(src) == dest) //We have arrived, no need to move again. - return TRUE - else if(dest != last_node) //The path should lead us to our given destination. If this is not true, we must stop. - set_path(null) - return FALSE - var/step_count = move_speed ? move_speed : base_speed //If a value is passed into move_speed, use that instead of the default speed var. - - if(step_count >= 1 && tries < BOT_STEP_MAX_RETRIES) - for(var/step_number in 1 to step_count) - addtimer(CALLBACK(src, PROC_REF(bot_step)), BOT_STEP_DELAY*(step_number-1)) - else - return FALSE - return TRUE - -/// Performs a step_towards and increments the path if successful. Returns TRUE if the bot moved and FALSE otherwise. -/mob/living/simple_animal/bot/proc/bot_step() - if(!length(path)) - return FALSE - - if(SEND_SIGNAL(src, COMSIG_MOB_BOT_PRE_STEP) & COMPONENT_MOB_BOT_BLOCK_PRE_STEP) - return FALSE - - if(!step_towards(src, path[1])) - tries++ - return FALSE - - increment_path() - tries = 0 - SEND_SIGNAL(src, COMSIG_MOB_BOT_STEP) - return TRUE - - -/mob/living/simple_animal/bot/proc/check_bot_access() - if(mode != BOT_SUMMON && mode != BOT_RESPONDING) - access_card.set_access(prev_access) - -/mob/living/simple_animal/bot/proc/call_bot(summoner, turf/waypoint, message = TRUE) - if(isAI(summoner) && calling_ai && calling_ai != src) //Prevents an override if another AI is controlling this bot. - return FALSE - - bot_reset() //Reset a bot before setting it to call mode. - - set_path(get_path_to(src, waypoint, max_distance=200, access = REGION_ACCESS_ALL_STATION)) - calling_ai = summoner //Link the AI to the bot! - ai_waypoint = waypoint - - if(path?.len) //Ensures that a valid path is calculated! - var/end_area = get_area_name(waypoint) - if(!(bot_mode_flags & BOT_MODE_ON)) - turn_on() //Saves the AI the hassle of having to activate a bot manually. - access_card.set_access(REGION_ACCESS_ALL_STATION) //Give the bot all-access while under the AI's command. - if(client) - reset_access_timer_id = addtimer(CALLBACK (src, PROC_REF(bot_reset)), 60 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE) //if the bot is player controlled, they get the extra access for a limited time - to_chat(src, span_notice("[span_big("Priority waypoint set by [icon2html(calling_ai, src)] [summoner]. Proceed to [end_area].")]
[path.len-1] meters to destination. You have been granted additional door access for 60 seconds.")) - if(message) - to_chat(calling_ai, span_notice("[icon2html(src, calling_ai)] [name] called to [end_area]. [path.len-1] meters to destination.")) - pathset = TRUE - mode = BOT_RESPONDING - tries = 0 - else - if(message) - to_chat(calling_ai, span_danger("Failed to calculate a valid route. Ensure destination is clear of obstructions and within range.")) - calling_ai = null - set_path(null) - - return TRUE - -/mob/living/simple_animal/bot/proc/call_mode() //Handles preparing a bot for a call, as well as calling the move proc. -//Handles the bot's movement during a call. - var/success = bot_move(ai_waypoint, 3) - if(!success) - if(calling_ai) - to_chat(calling_ai, "[icon2html(src, calling_ai)] [get_turf(src) == ai_waypoint ? span_notice("[src] successfully arrived to waypoint.") : span_danger("[src] failed to reach waypoint.")]") - calling_ai = null - bot_reset() - -/mob/living/simple_animal/bot/proc/bot_reset() - if(calling_ai) //Simple notification to the AI if it called a bot. It will not know the cause or identity of the bot. - to_chat(calling_ai, span_danger("Call command to a bot has been reset.")) - calling_ai = null - if(reset_access_timer_id) - deltimer(reset_access_timer_id) - reset_access_timer_id = null - set_path(null) - summon_target = null - pathset = FALSE - access_card.set_access(prev_access) - tries = 0 - mode = BOT_IDLE - ignore_list = list() - diag_hud_set_botstat() - diag_hud_set_botmode() - - - - -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -//Patrol and summon code! -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -/mob/living/simple_animal/bot/proc/bot_patrol() - patrol_step() - addtimer(CALLBACK(src, PROC_REF(do_patrol)), 0.5 SECONDS) - -/mob/living/simple_animal/bot/proc/do_patrol() - if(mode == BOT_PATROL) - patrol_step() - -/mob/living/simple_animal/bot/proc/start_patrol() - - if(tries >= BOT_STEP_MAX_RETRIES) //Bot is trapped, so stop trying to patrol. - bot_mode_flags &= ~BOT_MODE_AUTOPATROL - tries = 0 - speak("Unable to start patrol.") - - return - - if(!(bot_mode_flags & BOT_MODE_AUTOPATROL)) //A bot not set to patrol should not be patrolling. - mode = BOT_IDLE - return - - if(patrol_target) // has patrol target - INVOKE_ASYNC(src, PROC_REF(target_patrol)) - else // no patrol target, so need a new one - speak("Engaging patrol mode.") - find_patrol_target() - tries++ - return - -/mob/living/simple_animal/bot/proc/target_patrol() - calc_path() // Find a route to it - if(!path.len) - patrol_target = null - return - mode = BOT_PATROL -// perform a single patrol step - -/mob/living/simple_animal/bot/proc/patrol_step() - - if(client) // In use by player, don't actually move. - return - - if(loc == patrol_target) // reached target - //Find the next beacon matching the target. - if(!get_next_patrol_target()) - find_patrol_target() //If it fails, look for the nearest one instead. - return - - else if(path.len > 0 && patrol_target) // valid path - if(path[1] == loc) - increment_path() - return - - - var/moved = bot_move(patrol_target)//step_towards(src, next) // attempt to move - if(!moved) //Couldn't proceed the next step of the path BOT_STEP_MAX_RETRIES times - addtimer(CALLBACK(src, PROC_REF(patrol_step_not_moved)), 0.2 SECONDS) - - else // no path, so calculate new one - mode = BOT_START_PATROL - -/mob/living/simple_animal/bot/proc/patrol_step_not_moved() - calc_path() - if(!length(path)) - find_patrol_target() - tries = 0 - -// finds the nearest beacon to self -/mob/living/simple_animal/bot/proc/find_patrol_target() - nearest_beacon = null - new_destination = null - find_nearest_beacon() - if(nearest_beacon) - patrol_target = nearest_beacon_loc - destination = next_destination - else - bot_mode_flags &= ~BOT_MODE_AUTOPATROL - mode = BOT_IDLE - speak("Disengaging patrol mode.") - -/mob/living/simple_animal/bot/proc/get_next_patrol_target() - // search the beacon list for the next target in the list. - for(var/obj/machinery/navbeacon/NB in GLOB.navbeacons["[z]"]) - if(NB.location == next_destination) //Does the Beacon location text match the destination? - destination = new_destination //We now know the name of where we want to go. - patrol_target = NB.loc //Get its location and set it as the target. - next_destination = NB.codes[NAVBEACON_PATROL_NEXT] //Also get the name of the next beacon in line. - return TRUE - -/mob/living/simple_animal/bot/proc/find_nearest_beacon() - for(var/obj/machinery/navbeacon/NB in GLOB.navbeacons["[z]"]) - var/dist = get_dist(src, NB) - if(nearest_beacon) //Loop though the beacon net to find the true closest beacon. - //Ignore the beacon if were are located on it. - if(dist>1 && dist 1) //Begin the search, save this one for comparison on the next loop. - nearest_beacon = NB.location - nearest_beacon_loc = NB.loc - patrol_target = nearest_beacon_loc - destination = nearest_beacon - -//PDA control. Some bots, especially MULEs, may have more parameters. -/mob/living/simple_animal/bot/proc/bot_control(command, mob/user, list/user_access = list()) - if(!(bot_mode_flags & BOT_MODE_ON) || bot_cover_flags & BOT_COVER_EMAGGED || !(bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) //Emagged bots do not respect anyone's authority! Bots with their remote controls off cannot get commands. - return TRUE //ACCESS DENIED - if(client) - bot_control_message(command, user) - // process control input - switch(command) - if("patroloff") - bot_reset() //HOLD IT!! //OBJECTION!! - bot_mode_flags &= ~BOT_MODE_AUTOPATROL - - if("patrolon") - bot_mode_flags |= BOT_MODE_AUTOPATROL - - if("summon") - bot_reset() - summon_target = get_turf(user) - if(user_access.len != 0) - access_card.set_access(user_access + prev_access) //Adds the user's access, if any. - mode = BOT_SUMMON - speak("Responding.", radio_channel) - if("ejectpai") - ejectpairemote(user) - return - - -/mob/living/simple_animal/bot/proc/bot_control_message(command, user) - switch(command) - if("patroloff") - to_chat(src, "STOP PATROL") - if("patrolon") - to_chat(src, "START PATROL") - if("summon") - to_chat(src, "PRIORITY ALERT:[user] in [get_area_name(user)]!") - if("stop") - to_chat(src, "STOP!") - - if("go") - to_chat(src, "GO!") - - if("home") - to_chat(src, "RETURN HOME!") - if("ejectpai") - return - else - to_chat(src, span_warning("Unidentified control sequence received:[command]")) - -// calculates a path to the current destination -// given an optional turf to avoid -/mob/living/simple_animal/bot/proc/calc_path(turf/avoid) - check_bot_access() - set_path(get_path_to(src, patrol_target, max_distance=120, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL)) - -/mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid) - check_bot_access() - var/datum/callback/path_complete = CALLBACK(src, PROC_REF(on_summon_path_finish)) - SSpathfinder.pathfind(src, summon_target, max_distance=150, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL, on_finish=list(path_complete)) - -/mob/living/simple_animal/bot/proc/on_summon_path_finish(list/path) - set_path(path) - if(!length(path)) //Cannot reach target. Give up and announce the issue. - speak("Summon command failed, destination unreachable.",radio_channel) - bot_reset() - -/mob/living/simple_animal/bot/proc/summon_step() - - if(client) // In use by player, don't actually move. - return - - if(loc == summon_target) // Arrived to summon location. - bot_reset() - return - - else if(path.len > 0 && summon_target) //Proper path acquired! - if(path[1] == loc) - increment_path() - return - - var/moved = bot_move(summon_target, 3) // Move attempt - if(!moved) - addtimer(CALLBACK(src, PROC_REF(summon_step_not_moved)), 0.2 SECONDS) - - else // no path, so calculate new one - calc_summon_path() - -/mob/living/simple_animal/bot/proc/summon_step_not_moved() - calc_summon_path() - tries = 0 - -/mob/living/simple_animal/bot/proc/attempt_access(mob/bot, obj/door_attempt) - SIGNAL_HANDLER - - if(door_attempt.check_access(access_card)) - frustration = 0 - return ACCESS_ALLOWED - return ACCESS_DISALLOWED - -/mob/living/simple_animal/bot/ui_data(mob/user) - var/list/data = list() - data["can_hack"] = HAS_SILICON_ACCESS(user) - data["custom_controls"] = list() - data["emagged"] = bot_cover_flags & BOT_COVER_EMAGGED - data["has_access"] = allowed(user) - data["locked"] = bot_cover_flags & BOT_COVER_LOCKED - data["settings"] = list() - if(!(bot_cover_flags & BOT_COVER_LOCKED) || HAS_SILICON_ACCESS(user)) - data["settings"]["pai_inserted"] = !!paicard - data["settings"]["allow_possession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT - data["settings"]["possession_enabled"] = can_be_possessed - data["settings"]["airplane_mode"] = !(bot_mode_flags & BOT_MODE_REMOTE_ENABLED) - data["settings"]["maintenance_lock"] = !(bot_cover_flags & BOT_COVER_MAINTS_OPEN) - data["settings"]["power"] = bot_mode_flags & BOT_MODE_ON - data["settings"]["patrol_station"] = bot_mode_flags & BOT_MODE_AUTOPATROL - return data - -// Actions received from TGUI -/mob/living/simple_animal/bot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) - . = ..() - if(.) - return - var/mob/user = ui.user - if(!allowed(user)) - to_chat(user, span_warning("Access denied.")) - return - - if(action == "lock") - bot_cover_flags ^= BOT_COVER_LOCKED - - switch(action) - if("power") - if(bot_mode_flags & BOT_MODE_ON) - turn_off() - else - turn_on() - if("maintenance") - bot_cover_flags ^= BOT_COVER_MAINTS_OPEN - if("patrol") - bot_mode_flags ^= BOT_MODE_AUTOPATROL - bot_reset() - if("airplane") - bot_mode_flags ^= BOT_MODE_REMOTE_ENABLED - if("hack") - if(!HAS_SILICON_ACCESS(user)) - return - if(!(bot_cover_flags & BOT_COVER_EMAGGED)) - bot_cover_flags |= (BOT_COVER_EMAGGED|BOT_COVER_HACKED|BOT_COVER_LOCKED) - to_chat(user, span_warning("You overload [src]'s [hackables].")) - message_admins("Safety lock of [ADMIN_LOOKUPFLW(src)] was disabled by [ADMIN_LOOKUPFLW(user)] in [ADMIN_VERBOSEJMP(src)]") - user.log_message("disabled safety lock of [src]", LOG_GAME) - bot_reset() - to_chat(src, span_userdanger("(#$*#$^^( OVERRIDE DETECTED")) - to_chat(src, span_boldnotice(get_emagged_message())) - return - if(!(bot_cover_flags & BOT_COVER_HACKED)) - to_chat(user, span_bolddanger("You fail to repair [src]'s [hackables].")) - return - bot_cover_flags &= ~(BOT_COVER_EMAGGED|BOT_COVER_HACKED) - to_chat(user, span_notice("You reset the [src]'s [hackables].")) - user.log_message("re-enabled safety lock of [src]", LOG_GAME) - bot_reset() - to_chat(src, span_userdanger("Software restored to standard.")) - to_chat(src, span_boldnotice(possessed_message)) - if("eject_pai") - if(!paicard) - return - to_chat(user, span_notice("You eject [paicard] from [initial(src.name)].")) - ejectpai(user) - if("toggle_personality") - if (can_be_possessed) - disable_possession(user) - else - enable_possession(user) - if("rename") - rename(user) - -/mob/living/simple_animal/bot/update_icon_state() - icon_state = "[isnull(base_icon_state) ? initial(icon_state) : base_icon_state][get_bot_flag(bot_mode_flags, BOT_MODE_ON)]" - return ..() - -/// Access check proc for bot topics! Remember to place in a bot's individual Topic if desired. -/mob/living/simple_animal/bot/proc/topic_denied(mob/user) - if(!user.can_perform_action(src, ALLOW_SILICON_REACH)) - return TRUE - // 0 for access, 1 for denied. - if(bot_cover_flags & BOT_COVER_EMAGGED) //An emagged bot cannot be controlled by humans, silicons can if one hacked it. - if(!(bot_cover_flags & BOT_COVER_HACKED)) //Manually emagged by a human - access denied to all. - return TRUE - else if(!HAS_SILICON_ACCESS(user)) //Bot is hacked, so only silicons and admins are allowed access. - return TRUE - return FALSE - -/// Places a pAI in control of this mob -/mob/living/simple_animal/bot/proc/insertpai(mob/user, obj/item/pai_card/card) - if(paicard) - balloon_alert(user, "slot occupied!") - return - if(key) - balloon_alert(user, "personality already present!") - return - if(bot_cover_flags & BOT_COVER_LOCKED || !(bot_cover_flags & BOT_COVER_MAINTS_OPEN)) - balloon_alert(user, "slot inaccessible!") - return - if(!(bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT)) - balloon_alert(user, "incompatible firmware!") - return - if(!card.pai || !card.pai.mind) - balloon_alert(user, "pAI is inactive!") - return - if(!user.transferItemToLoc(card, src)) - return - paicard = card - disable_possession() - if(paicard.pai.holoform) - paicard.pai.fold_in() - copy_languages(paicard.pai, source_override = LANGUAGE_PAI) - set_active_language(paicard.pai.get_selected_language()) - user.visible_message(span_notice("[user] inserts [card] into [src]!"), span_notice("You insert [card] into [src].")) - paicard.pai.mind.transfer_to(src) - to_chat(src, span_notice("You sense your form change as you are uploaded into [src].")) - name = paicard.pai.name - original_faction = get_faction() - original_allies = allies - SET_FACTION_AND_ALLIES_FROM(src, user) - log_combat(user, paicard.pai, "uploaded to [initial(src.name)],") - return TRUE - -/mob/living/simple_animal/bot/ghost() - if(stat != DEAD) // Only ghost if we're doing this while alive, the pAI probably isn't dead yet. - return ..() - if(paicard && (!client || stat == DEAD)) - ejectpai() - -/// Ejects a pAI from this bot -/mob/living/simple_animal/bot/proc/ejectpai(mob/user = null, announce = TRUE) - if(!paicard) - return - if(mind && paicard.pai) - mind.transfer_to(paicard.pai) - else if(paicard.pai) - paicard.pai.PossessByPlayer(key) - else - ghostize(FALSE) // The pAI card that just got ejected was dead. - key = null - paicard.forceMove(loc) - if(user) - log_combat(user, paicard.pai, "ejected from [initial(src.name)],") - else - log_combat(src, paicard.pai, "ejected") - if(announce) - to_chat(paicard.pai, span_notice("You feel your control fade as [paicard] ejects from [initial(src.name)].")) - paicard = null - name = initial(src.name) - set_faction(original_faction) - set_allies(original_allies) - remove_all_languages(source = LANGUAGE_PAI) - get_selected_language() - -/// Ejects the pAI remotely. -/mob/living/simple_animal/bot/proc/ejectpairemote(mob/user) - if(!allowed(user) || !paicard) - return - speak("Ejecting personality chip.", radio_channel) - ejectpai(user) - -/mob/living/simple_animal/bot/Login() - . = ..() - if(!. || !client) - return FALSE - // If we have any bonus player accesses, add them to our internal ID card. - if(length(player_access)) - access_card.add_access(player_access) - diag_hud_set_botmode() - -/mob/living/simple_animal/bot/Logout() - . = ..() - bot_reset() - -/mob/living/simple_animal/bot/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE) - . = ..() - if(!.) - return - update_appearance() - -/mob/living/simple_animal/bot/sentience_act() - remove_faction(FACTION_SILICON) - -/mob/living/simple_animal/bot/proc/set_path(list/newpath) - path = newpath ? newpath : list() - if(!path_hud) - return - var/list/path_huds_watching_me = list(GLOB.huds[DATA_HUD_DIAGNOSTIC], GLOB.huds[DATA_HUD_BOT_PATH]) - if(path_hud) - path_huds_watching_me += path_hud - for(var/datum/atom_hud/hud as anything in path_huds_watching_me) - hud.remove_atom_from_hud(src) - - var/list/path_images = active_hud_list[DIAG_PATH_HUD] - LAZYCLEARLIST(path_images) - if(length(newpath)) - var/mutable_appearance/path_image = mutable_appearance(path_image_icon, path_image_icon_state, BOT_PATH_LAYER, appearance_flags = RESET_COLOR|RESET_TRANSFORM|KEEP_APART) - path_image.color = path_image_color - for(var/i in 1 to newpath.len) - var/turf/T = newpath[i] - if(T == loc) //don't bother putting an image if it's where we already exist. - continue - var/direction = get_dir(src, T) - if(i > 1) - var/turf/prevT = path[i - 1] - var/image/prevI = path[prevT] - direction = get_dir(prevT, T) - if(i > 2 && prevI) // make sure we actually have an image to manipulate at index > 2 - var/turf/prevprevT = path[i - 2] - var/prevDir = get_dir(prevprevT, prevT) - var/mixDir = direction|prevDir - if(ISDIAGONALDIR(mixDir)) - prevI.dir = mixDir - if(prevDir & (NORTH|SOUTH)) - var/matrix/ntransform = matrix() - ntransform.Turn(90) - if((mixDir == NORTHWEST) || (mixDir == SOUTHEAST)) - ntransform.Scale(-1, 1) - else - ntransform.Scale(1, -1) - prevI.transform = ntransform - - SET_PLANE(path_image, GAME_PLANE, T) - path_image.dir = direction - var/image/I = image(loc = T) - I.appearance = path_image - path[T] = I - path_images += I - - for(var/datum/atom_hud/hud as anything in path_huds_watching_me) - hud.add_atom_to_hud(src) - -/mob/living/simple_animal/bot/proc/increment_path() - if(!length(path)) - return - var/image/I = path[path[1]] - if(I) - animate(I, alpha = 0, time = 3) - path.Cut(1, 2) - - if(!length(path)) - addtimer(CALLBACK(src, PROC_REF(set_path), null), 0.6 SECONDS) // Enough time for the animate to finish - -/mob/living/simple_animal/bot/rust_heretic_act() - adjust_brute_loss(400) - -/mob/living/simple_animal/bot/get_hit_area_message(input_area) - // we just get hit, there's no complexity for hitting an arm (if it exists) or anything. - // we also need to return an empty string as otherwise it would falsely say that we get hit in the chest or something strange like that (bots don't have "chests") - return "" - -//Will always check hands first, because access_card is internal to the mob and can't be removed or swapped. -/mob/living/simple_animal/bot/get_idcard(hand_first) - return (..() || access_card) diff --git a/code/modules/mob/living/simple_animal/bot/bot_announcement.dm b/code/modules/mob/living/simple_animal/bot/bot_announcement.dm index 0a9f8d8d59c2..10161dddfd9d 100644 --- a/code/modules/mob/living/simple_animal/bot/bot_announcement.dm +++ b/code/modules/mob/living/simple_animal/bot/bot_announcement.dm @@ -51,7 +51,7 @@ /datum/action/cooldown/bot_announcement/ui_data(mob/user) var/list/data = list() - var/mob/living/simple_animal/bot/bot_owner = owner + var/mob/living/basic/bot/bot_owner = owner if(istype(bot_owner)) var/list/channels = list() for(var/channel in bot_owner.internal_radio.channels) @@ -149,7 +149,7 @@ /// Speak the provided line on the provided radio channel /datum/action/cooldown/bot_announcement/proc/announce(line, channel) - var/mob/living/simple_animal/bot/bot_owner = owner + var/mob/living/basic/bot/bot_owner = owner if (!(bot_owner.bot_mode_flags & BOT_MODE_ON)) return diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm deleted file mode 100644 index 5095a1e4baa7..000000000000 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ /dev/null @@ -1,873 +0,0 @@ - - -// Mulebot - carries crates around for Quartermaster -// Navigates via floor navbeacons -// Remote Controlled from QM's PDA - -#define SIGH 0 -#define ANNOYED 1 -#define DELIGHT 2 -#define CHIME 3 - -/mob/living/simple_animal/bot/mulebot - name = "\improper MULEbot" - desc = "A Multiple Utility Load Effector bot." - icon_state = "mulebot0" - light_color = "#ffcc99" - light_power = 0.8 - density = TRUE - move_resist = MOVE_FORCE_STRONG - animate_movement = SLIDE_STEPS - health = 50 - maxHealth = 50 - speed = 3 - damage_coeff = list(BRUTE = 0.5, BURN = 0.7, TOX = 0, STAMINA = 0, OXY = 0) - combat_mode = TRUE //No swapping - buckle_lying = 0 - mob_size = MOB_SIZE_LARGE - buckle_prevents_pull = TRUE // No pulling loaded shit - bot_mode_flags = ~BOT_MODE_ROUNDSTART_POSSESSION - req_one_access = list(ACCESS_ROBOTICS, ACCESS_CARGO) - radio_key = /obj/item/encryptionkey/headset_cargo - radio_channel = RADIO_CHANNEL_SUPPLY - bot_type = MULE_BOT - path_image_color = "#7F5200" - possessed_message = "You are a MULEbot! Do your best to make sure that packages get to their destination!" - - /// unique identifier in case there are multiple mulebots. - var/id - - var/base_icon = "mulebot" /// icon_state to use in update_icon_state - var/atom/movable/load /// what we're transporting - var/mob/living/passenger /// who's riding us - var/turf/target /// this is turf to navigate to (location of beacon) - var/loaddir = 0 /// this the direction to unload onto/load from - var/home_destination = "" /// tag of home delivery beacon - - var/reached_target = TRUE ///true if already reached the target - ///Number of times retried a blocked path - var/blockcount = 0 - - ///flags of mulebot mode - var/mulebot_delivery_flags = MULEBOT_RETURN_MODE | MULEBOT_AUTO_PICKUP_MODE | MULEBOT_REPORT_DELIVERY_MODE - - var/obj/item/stock_parts/power_store/cell /// Internal Powercell - var/cell_move_power_usage = 1///How much power we use when we move. - var/num_steps = 0 ///The amount of steps we should take until we rest for a time. - - - -/mob/living/simple_animal/bot/mulebot/Initialize(mapload) - . = ..() - - RegisterSignal(src, COMSIG_MOB_BOT_PRE_STEP, PROC_REF(check_pre_step)) - RegisterSignal(src, COMSIG_MOB_CLIENT_PRE_MOVE, PROC_REF(check_pre_step)) - RegisterSignal(src, COMSIG_MOB_BOT_STEP, PROC_REF(on_bot_step)) - RegisterSignal(src, COMSIG_MOB_CLIENT_MOVED, PROC_REF(on_bot_step)) - - ADD_TRAIT(src, TRAIT_NOMOBSWAP, INNATE_TRAIT) - - if(prob(0.666) && mapload) - new /mob/living/simple_animal/bot/mulebot/paranormal(loc) - return INITIALIZE_HINT_QDEL - set_wires(new /datum/wires/mulebot(src)) - - // Doing this hurts my soul, but simplebot access reworks are for another day. - var/datum/id_trim/job/cargo_trim = SSid_access.trim_singletons_by_path[/datum/id_trim/job/cargo_technician] - access_card.add_access(cargo_trim.access + cargo_trim.wildcard_access) - prev_access = access_card.access.Copy() - - cell = new /obj/item/stock_parts/power_store/cell/upgraded(src, 2000) - - AddElement(/datum/element/ridable, /datum/component/riding/creature/mulebot) - diag_hud_set_mulebotcell() - - set_id(suffix || assign_random_name()) - suffix = null - if(name == "\improper MULEbot") - name = "\improper MULEbot [id]" - set_home(get_turf(src)) - -/mob/living/simple_animal/bot/mulebot/Exited(atom/movable/gone, direction) - . = ..() - if(gone == load) - unload(0) - if(gone == cell) - turn_off() - cell = null - diag_hud_set_mulebotcell() - -/mob/living/simple_animal/bot/mulebot/examine(mob/user) - . = ..() - if(bot_cover_flags & BOT_COVER_MAINTS_OPEN) - if(cell) - . += span_notice("[p_They()] [p_have()] \a [cell] installed.") - . += span_info("You can use a crowbar to remove it.") - else - . += span_notice("[p_They()] [p_have()] an empty compartment where a power cell can be installed.") - if(load) //observer check is so we don't show the name of the ghost that's sitting on it to prevent metagaming who's ded. - . += span_notice("\A [isobserver(load) ? "ghostly figure" : load] is on [p_their()] load platform.") - - -/mob/living/simple_animal/bot/mulebot/Destroy() - UnregisterSignal(src, list(COMSIG_MOB_BOT_PRE_STEP, COMSIG_MOB_CLIENT_PRE_MOVE, COMSIG_MOB_BOT_STEP, COMSIG_MOB_CLIENT_MOVED)) - unload(0) - QDEL_NULL(cell) - return ..() - -/mob/living/simple_animal/bot/mulebot/get_cell() - return cell - -/mob/living/simple_animal/bot/mulebot/turn_on() - if(!has_power()) - return - return ..() - -/// returns true if the bot is fully powered. -/mob/living/simple_animal/bot/mulebot/proc/has_power() - return cell && cell.charge > 0 && (!wires.is_cut(WIRE_POWER1) && !wires.is_cut(WIRE_POWER2)) - -/mob/living/simple_animal/bot/mulebot/attack_hand(mob/living/carbon/human/user, list/modifiers) - if(bot_cover_flags & BOT_COVER_MAINTS_OPEN && !HAS_AI_ACCESS(user)) - wires.interact(user) - return - if(wires.is_cut(WIRE_RX) && HAS_AI_ACCESS(user)) - return - - return ..() - -/mob/living/simple_animal/bot/mulebot/proc/set_id(new_id) - id = new_id - -/mob/living/simple_animal/bot/mulebot/proc/set_home(turf/home_loc) - if(!istype(home_loc)) - CRASH("MULEbot [id] was requested to set a home location to [home_loc ? "an invalid home loc ([home_loc.type])" : "null"]") - - var/obj/machinery/navbeacon/home_beacon = locate() in home_loc - if(!isnull(home_beacon)) - home_destination = home_beacon.location - log_transport("[id]: MULEbot successfuly set home location to ID [home_destination] at [home_beacon.x], [home_beacon.y], [home_beacon.z]") - return - - log_transport("[id]: MULEbot failed to set home at [home_loc.x], [home_loc.y], [home_loc.z]") - -/mob/living/simple_animal/bot/mulebot/bot_reset() - ..() - reached_target = FALSE - -/mob/living/simple_animal/bot/mulebot/screwdriver_act(mob/living/user, obj/item/tool) - . = ..() - update_appearance() - -/mob/living/simple_animal/bot/mulebot/crowbar_act(mob/living/user, obj/item/tool) - if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN) || user.combat_mode) - return - if(!cell) - to_chat(user, span_warning("[src] doesn't have a power cell!")) - return ITEM_INTERACT_BLOCKING - cell.add_fingerprint(user) - user.visible_message( - span_notice("[user] crowbars [cell] out from [src]."), - span_notice("You pry [cell] out of [src]."), - ) - if(Adjacent(user) && !issilicon(user)) - user.put_in_hands(cell) - else - cell.forceMove(drop_location()) - return ITEM_INTERACT_SUCCESS - -/mob/living/simple_animal/bot/mulebot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) - if(istype(tool, /obj/item/stock_parts/power_store/cell) && (bot_cover_flags & BOT_COVER_MAINTS_OPEN)) - if(cell) - to_chat(user, span_warning("[src] already has a power cell!")) - return ITEM_INTERACT_BLOCKING - if(!user.transferItemToLoc(tool, src)) - return ITEM_INTERACT_BLOCKING - cell = tool - diag_hud_set_mulebotcell() - user.visible_message( - span_notice("[user] inserts \a [cell] into [src]."), - span_notice("You insert [cell] into [src]."), - ) - return ITEM_INTERACT_SUCCESS - if(is_wire_tool(tool) && (bot_cover_flags & BOT_COVER_MAINTS_OPEN)) - attack_hand(user) - return ITEM_INTERACT_SUCCESS - return ..() - -/mob/living/simple_animal/bot/mulebot/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) - . = ..() - if(ismob(load) && prob(1 + attacking_item.force * 2)) - user.visible_message( - span_danger("[user] knocks [load] off [src] with \the [attacking_item]!"), - span_danger("You knock [load] off [src] with \the [attacking_item]!"), - ) - unload(0) - return TRUE - -/mob/living/simple_animal/bot/mulebot/emag_act(mob/user, obj/item/card/emag/emag_card) - if(!(bot_cover_flags & BOT_COVER_EMAGGED)) - bot_cover_flags |= BOT_COVER_EMAGGED - if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN)) - bot_cover_flags ^= BOT_COVER_LOCKED - balloon_alert(user, "controls [bot_cover_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"]") - flick("[base_icon]-emagged", src) - playsound(src, SFX_SPARKS, 100, FALSE, SHORT_RANGE_SOUND_EXTRARANGE) - return TRUE - -/mob/living/simple_animal/bot/mulebot/update_icon_state() //if you change the icon_state names, please make sure to update /datum/wires/mulebot/on_pulse() as well. <3 - . = ..() - icon_state = "[base_icon][(bot_mode_flags & BOT_MODE_ON) ? wires.is_cut(WIRE_AVOIDANCE) : 0]" - -/mob/living/simple_animal/bot/mulebot/update_overlays() - . = ..() - if(bot_cover_flags & BOT_COVER_MAINTS_OPEN) - . += "[base_icon]-hatch" - if(!load || ismob(load)) //mob offsets and such are handled by the riding component / buckling - return - var/mutable_appearance/load_overlay = mutable_appearance(load.icon, load.icon_state, layer + 0.01) - load_overlay.pixel_z = initial(load.pixel_z) + 11 - . += load_overlay - -/mob/living/simple_animal/bot/mulebot/ex_act(severity) - unload(0) - switch(severity) - if(EXPLODE_DEVASTATE) - qdel(src) - if(EXPLODE_HEAVY) - wires.cut_random() - wires.cut_random() - if(EXPLODE_LIGHT) - wires.cut_random() - - return TRUE - - -/mob/living/simple_animal/bot/mulebot/bullet_act(obj/projectile/proj) - . = ..() - if(. && !QDELETED(src)) //Got hit and not blown up yet. - if(prob(50) && !isnull(load)) - unload(0) - if(prob(25)) - visible_message(span_danger("Something shorts out inside [src]!")) - wires.cut_random(source = proj.firer) - -/mob/living/simple_animal/bot/mulebot/ui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "Mule", name) - ui.open() - -/mob/living/simple_animal/bot/mulebot/ui_data(mob/user) - var/list/data = list() - data["powerStatus"] = bot_mode_flags & BOT_MODE_ON - data["locked"] = bot_cover_flags & BOT_COVER_LOCKED - data["siliconUser"] = HAS_SILICON_ACCESS(user) - data["mode"] = mode ? "[mode]" : "Ready" - data["modeStatus"] = "" - switch(mode) - if(BOT_IDLE, BOT_DELIVER, BOT_GO_HOME) - data["modeStatus"] = "good" - if(BOT_BLOCKED, BOT_NAV, BOT_WAIT_FOR_NAV) - data["modeStatus"] = "average" - if(BOT_NO_ROUTE) - data["modeStatus"] = "bad" - data["load"] = get_load_name() - data["destination"] = destination - data["homeDestination"] = home_destination - data["destinationsList"] = GLOB.deliverybeacontags - data["cellPercent"] = cell?.percent() - data["autoReturn"] = mulebot_delivery_flags & MULEBOT_RETURN_MODE - data["autoPickup"] = mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE - data["reportDelivery"] = mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE - data["botId"] = id - data["allowPossession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT - data["possessionEnabled"] = can_be_possessed - data["paiInserted"] = !!paicard - return data - -/mob/living/simple_animal/bot/mulebot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) - . = ..() - var/mob/user = ui.user - if(. || (bot_cover_flags & BOT_COVER_LOCKED && !HAS_SILICON_ACCESS(user))) - return - - switch(action) - if("lock") - if(HAS_SILICON_ACCESS(user)) - bot_cover_flags ^= BOT_COVER_LOCKED - return TRUE - if("on") - if(bot_mode_flags & BOT_MODE_ON) - turn_off() - else if(bot_cover_flags & BOT_COVER_MAINTS_OPEN) - to_chat(user, span_warning("[name]'s maintenance panel is open!")) - return - else if(cell) - if(!turn_on()) - to_chat(user, span_warning("You can't switch on [src]!")) - return - return TRUE - else - bot_control(action, user, params) // Kill this later. // Kill PDAs in general please - return TRUE - -/mob/living/simple_animal/bot/mulebot/bot_control(command, mob/user, list/params = list(), pda = FALSE) - if(pda && wires.is_cut(WIRE_RX)) // MULE wireless is controlled by wires. - return - - switch(command) - if("stop") - if(mode != BOT_IDLE) - bot_reset() - if("go") - if(mode == BOT_IDLE) - start() - if("home") - if(mode == BOT_IDLE || mode == BOT_DELIVER) - start_home() - if("destination") - var/new_dest - if(pda) - new_dest = tgui_input_list(user, "Enter Destination", "Mulebot Settings", GLOB.deliverybeacontags, destination) - else - new_dest = params["value"] - if(new_dest) - set_destination(new_dest) - if("setid") - var/new_id = tgui_input_text(user, "Enter ID", "ID Assignment", id, max_length = MAX_NAME_LEN) - if(new_id) - set_id(new_id) - name = "\improper MULEbot [new_id]" - if("sethome") - var/new_home = tgui_input_list(user, "Enter Home", "Mulebot Settings", GLOB.deliverybeacontags, home_destination) - if(new_home) - home_destination = new_home - if("unload") - if(load && mode != BOT_HUNT) - if(loc == target) - unload(loaddir) - else - unload(0) - if("autoret") - mulebot_delivery_flags ^= MULEBOT_RETURN_MODE - if("autopick") - mulebot_delivery_flags ^= MULEBOT_AUTO_PICKUP_MODE - if("report") - mulebot_delivery_flags ^= MULEBOT_REPORT_DELIVERY_MODE - -/mob/living/simple_animal/bot/mulebot/proc/buzz(type) - switch(type) - if(SIGH) - audible_message(span_hear("[src] makes a sighing buzz.")) - playsound(src, 'sound/machines/buzz/buzz-sigh.ogg', 50, FALSE) - if(ANNOYED) - audible_message(span_hear("[src] makes an annoyed buzzing sound.")) - playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, FALSE) - if(DELIGHT) - audible_message(span_hear("[src] makes a delighted ping!")) - playsound(src, 'sound/machines/ping.ogg', 50, FALSE) - if(CHIME) - audible_message(span_hear("[src] makes a chiming sound!")) - playsound(src, 'sound/machines/chime.ogg', 50, FALSE) - flick("[base_icon]1", src) - - -// mousedrop a crate to load the bot -// can load anything if hacked -/mob/living/simple_animal/bot/mulebot/mouse_drop_receive(atom/movable/AM, mob/user, params) - if(!isliving(user)) - return - - if(!istype(AM) || isdead(AM) || iseyemob(AM) || istype(AM, /obj/effect/dummy/phased_mob)) - return - - load(AM) - -// called to load a crate -/mob/living/simple_animal/bot/mulebot/proc/load(atom/movable/AM) - if(load || AM.anchored) - return - - if(!isturf(AM.loc)) //To prevent the loading from stuff from someone's inventory or screen icons. - return - - var/obj/structure/closet/crate/crate = AM - if(!istype(crate)) - if(!wires.is_cut(WIRE_LOADCHECK)) - buzz(SIGH) - return // if not hacked, only allow crates to be loaded - crate = null - - if(crate || isobj(AM)) - var/obj/O = AM - if(O.has_buckled_mobs() || (locate(/mob) in AM)) //can't load non crates objects with mobs buckled to it or inside it. - buzz(SIGH) - return - - if(crate) - crate.close() //make sure the crate is closed - - O.forceMove(src) - - else if(isliving(AM)) - if(!load_mob(AM)) //forceMove() is handled in buckling - return - - load = AM - mode = BOT_IDLE - update_appearance() - -///resolves the name to display for the loaded mob. primarily needed for the paranormal subtype since we don't want to show the name of ghosts riding it. -/mob/living/simple_animal/bot/mulebot/proc/get_load_name() - return load ? load.name : null - -/mob/living/simple_animal/bot/mulebot/proc/load_mob(mob/living/M) - can_buckle = TRUE - if(buckle_mob(M)) - passenger = M - load = M - can_buckle = FALSE - return TRUE - -/mob/living/simple_animal/bot/mulebot/post_unbuckle_mob(mob/living/M) - load = null - return ..() - -// called to unload the bot -// argument is optional direction to unload -// if zero, unload at bot's location -/mob/living/simple_animal/bot/mulebot/proc/unload(dirn) - if(QDELETED(load)) - if(load) //if our thing was qdel'd, there's likely a leftover reference. just clear it and remove the overlay. we'll let the bot keep moving around to prevent it abruptly stopping somewhere. - load = null - update_appearance() - return - - mode = BOT_IDLE - - var/atom/movable/cached_load = load //cache the load since unbuckling mobs clears the var. - - unbuckle_all_mobs() - - if(load) //don't have to do any of this for mobs. - load = null - cached_load.forceMove(loc) - cached_load.pixel_y = initial(cached_load.pixel_y) - cached_load.layer = initial(cached_load.layer) - SET_PLANE_EXPLICIT(cached_load, initial(cached_load.plane), src) - - if(dirn) //move the thing to the delivery point. - cached_load.Move(get_step(loc,dirn), dirn) - - update_appearance() - -/mob/living/simple_animal/bot/mulebot/get_status_tab_items() - . = ..() - if(cell) - . += "Charge Left: [cell.charge]/[cell.maxcharge]" - else - . += "No Cell Inserted!" - if(load) - . += "Current Load: [get_load_name()]" - - -/mob/living/simple_animal/bot/mulebot/call_bot() - ..() - if(path && length(path)) - target = ai_waypoint //Target is the end point of the path, the waypoint set by the AI. - destination = get_area_name(target, TRUE) - pathset = TRUE //Indicates the AI's custom path is initialized. - start() - -/mob/living/simple_animal/bot/mulebot/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) - . = ..() - if(has_gravity()) - for(var/mob/living/carbon/human/future_pancake in loc) - if(future_pancake.body_position == LYING_DOWN) - run_over(future_pancake) - - diag_hud_set_mulebotcell() - -/mob/living/simple_animal/bot/mulebot/handle_automated_action() - if(!(bot_mode_flags & BOT_MODE_ON)) - return - if(!has_power()) - turn_off() - return - if(mode == BOT_IDLE) - return - if(HAS_TRAIT(src, TRAIT_IMMOBILIZED)) - return - - var/speed = (wires.is_cut(WIRE_MOTOR1) ? 0 : 2) + (wires.is_cut(WIRE_MOTOR2) ? 0 : 1) - if(!speed)//Devide by zero man bad - return - num_steps = round(10/speed) //10, 5, or 3 steps, depending on how many wires we have cut - START_PROCESSING(SSfastprocess, src) - -/mob/living/simple_animal/bot/mulebot/process() - if(!(bot_mode_flags & BOT_MODE_ON) || client || (num_steps <= 0) || !has_power()) - return PROCESS_KILL - num_steps-- - - switch(mode) - if(BOT_IDLE) // idle - return - - if(BOT_DELIVER, BOT_GO_HOME, BOT_BLOCKED) // navigating to deliver,home, or blocked - if(loc == target) // reached target - at_target() - return - - else if(length(path) && target) // valid path - var/turf/next = path[1] - reached_target = FALSE - if(next == loc) - path -= next - return - if(isturf(next)) - if(SEND_SIGNAL(src, COMSIG_MOB_BOT_PRE_STEP) & COMPONENT_MOB_BOT_BLOCK_PRE_STEP) - return - var/oldloc = loc - var/moved = step_towards(src, next) // attempt to move - if(moved && oldloc != loc) // successful move - SEND_SIGNAL(src, COMSIG_MOB_BOT_STEP) - blockcount = 0 - path -= loc - if(destination == home_destination) - mode = BOT_GO_HOME - else - mode = BOT_DELIVER - - else // failed to move - - blockcount++ - mode = BOT_BLOCKED - if(blockcount == 3) - buzz(ANNOYED) - - if(blockcount > 10) // attempt 10 times before recomputing - // find new path excluding blocked turf - buzz(SIGH) - mode = BOT_WAIT_FOR_NAV - blockcount = 0 - addtimer(CALLBACK(src, PROC_REF(process_blocked), next), 2 SECONDS) - return - return - else - buzz(ANNOYED) - mode = BOT_NAV - return - else - mode = BOT_NAV - return - - if(BOT_NAV) // calculate new path - mode = BOT_WAIT_FOR_NAV - INVOKE_ASYNC(src, PROC_REF(process_nav)) - -/mob/living/simple_animal/bot/mulebot/proc/process_blocked(turf/next) - calc_path(avoid=next) - if(length(path)) - buzz(DELIGHT) - mode = BOT_BLOCKED - -/mob/living/simple_animal/bot/mulebot/proc/process_nav() - calc_path() - - if(length(path)) - blockcount = 0 - mode = BOT_BLOCKED - buzz(DELIGHT) - - else - buzz(SIGH) - - mode = BOT_NO_ROUTE - -// calculates a path to the current destination -// given an optional turf to avoid -/mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null) - path = get_path_to(src, target, max_distance=250, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL) - -// sets the current destination -// signals all beacons matching the delivery code -// beacons will return a signal giving their locations -/mob/living/simple_animal/bot/mulebot/proc/set_destination(new_dest) - new_destination = new_dest - get_nav() - -// starts bot moving to current destination -/mob/living/simple_animal/bot/mulebot/proc/start() - if(!(bot_mode_flags & BOT_MODE_ON)) - return - if(destination == home_destination) - mode = BOT_GO_HOME - else - mode = BOT_DELIVER - get_nav() - -// starts bot moving to home -// sends a beacon query to find -/mob/living/simple_animal/bot/mulebot/proc/start_home() - if(!(bot_mode_flags & BOT_MODE_ON)) - return - INVOKE_ASYNC(src, PROC_REF(do_start_home)) - -/mob/living/simple_animal/bot/mulebot/proc/do_start_home() - set_destination(home_destination) - mode = BOT_BLOCKED - -// called when bot reaches current target -/mob/living/simple_animal/bot/mulebot/proc/at_target() - if(!reached_target) - radio_channel = RADIO_CHANNEL_SUPPLY //Supply channel - buzz(CHIME) - reached_target = TRUE - - if(pathset) //The AI called us here, so notify it of our arrival. - loaddir = dir //The MULE will attempt to load a crate in whatever direction the MULE is "facing". - if(calling_ai) - to_chat(calling_ai, span_notice("[icon2html(src, calling_ai)] [src] wirelessly plays a chiming sound!")) - calling_ai.playsound_local(calling_ai, 'sound/machines/chime.ogg', 40, FALSE) - calling_ai = null - radio_channel = RADIO_CHANNEL_AI_PRIVATE //Report on AI Private instead if the AI is controlling us. - - if(load) // if loaded, unload at target - if(mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE) - speak("Destination [RUNECHAT_BOLD("[destination]")] reached. Unloading [load].",radio_channel) - unload(loaddir) - else - // not loaded - if(mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE) // find a crate - var/atom/movable/AM - if(wires.is_cut(WIRE_LOADCHECK)) // if hacked, load first unanchored thing we find - for(var/atom/movable/A in get_step(loc, loaddir)) - if(!A.anchored) - AM = A - break - else // otherwise, look for crates only - AM = locate(/obj/structure/closet/crate) in get_step(loc,loaddir) - if(AM?.Adjacent(src)) - load(AM) - if(mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE) - speak("Now loading [load] at [RUNECHAT_BOLD("[get_area_name(src)]")].", radio_channel) - // whatever happened, check to see if we return home - - if((mulebot_delivery_flags & MULEBOT_RETURN_MODE) && home_destination && destination != home_destination) - // auto return set and not at home already - start_home() - mode = BOT_BLOCKED - else - bot_reset() // otherwise go idle - - -/mob/living/simple_animal/bot/mulebot/MobBump(mob/M) // called when the bot bumps into a mob - if(mind || !isliving(M)) //if there's a sentience controlling the bot, they aren't allowed to harm folks. - return ..() - var/mob/living/L = M - if(wires.is_cut(WIRE_AVOIDANCE)) // usually just bumps, but if the avoidance wire is cut, knocks them over. - if(iscyborg(L)) - visible_message(span_danger("[src] bumps into [L]!")) - else if(L.Knockdown(8 SECONDS)) - log_combat(src, L, "knocked down") - visible_message(span_danger("[src] knocks over [L]!")) - return ..() - -// when mulebot is in the same loc -/mob/living/simple_animal/bot/mulebot/proc/run_over(mob/living/carbon/human/crushed) - if (!(bot_cover_flags & BOT_COVER_EMAGGED) && !wires.is_cut(WIRE_AVOIDANCE)) - if (!has_status_effect(/datum/status_effect/careful_driving)) - crushed.visible_message(span_notice("[src] slows down to avoid crushing [crushed].")) - apply_status_effect(/datum/status_effect/careful_driving) - return // Player mules must be emagged before they can trample - - log_combat(src, crushed, "run over", addition = "(DAMTYPE: [uppertext(BRUTE)])") - crushed.visible_message( - span_danger("[src] drives over [crushed]!"), - span_userdanger("[src] drives over you!"), - ) - - playsound(src, 'sound/effects/splat.ogg', 50, TRUE) - - var/damage = rand(5, 15) - crushed.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD, run_armor_check(BODY_ZONE_HEAD, MELEE)) - crushed.apply_damage(2 * damage, BRUTE, BODY_ZONE_CHEST, run_armor_check(BODY_ZONE_CHEST, MELEE)) - crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG, run_armor_check(BODY_ZONE_L_LEG, MELEE)) - crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG, run_armor_check(BODY_ZONE_R_LEG, MELEE)) - crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM, run_armor_check(BODY_ZONE_L_ARM, MELEE)) - crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM, run_armor_check(BODY_ZONE_R_ARM, MELEE)) - - add_mob_blood(crushed) - - var/turf/below_us = get_turf(src) - below_us.add_mob_blood(crushed) - - AddComponent(/datum/component/blood_walk, \ - blood_type = /obj/effect/decal/cleanable/blood/tracks, \ - target_dir_change = TRUE, \ - transfer_blood_dna = TRUE, \ - max_blood = 4) - -// player on mulebot attempted to move -/mob/living/simple_animal/bot/mulebot/relaymove(mob/living/user, direction) - if(user.incapacitated) - return - if(load == user) - unload(0) - - -//Update navigation data. Called when commanded to deliver, return home, or a route update is needed... -/mob/living/simple_animal/bot/mulebot/proc/get_nav() - if(!(bot_mode_flags & BOT_MODE_ON) || wires.is_cut(WIRE_BEACON)) - return - - for(var/obj/machinery/navbeacon/NB in GLOB.deliverybeacons) - if(NB.location == new_destination) // if the beacon location matches the set destination - // the we will navigate there - destination = new_destination - target = NB.loc - var/direction = NB.codes[NAVBEACON_DELIVERY_DIRECTION] // this will be the load/unload dir - if(!direction) - direction = NB.dir // fallback - if(direction) - loaddir = text2num(direction) - else - loaddir = 0 - if(destination) // No need to calculate a path if you do not have a destination set! - calc_path() - -/mob/living/simple_animal/bot/mulebot/emp_act(severity) - . = ..() - if(cell && !(. & EMP_PROTECT_CONTENTS)) - cell.emp_act(severity) - if(load) - load.emp_act(severity) - - -/mob/living/simple_animal/bot/mulebot/explode() - var/atom/Tsec = drop_location() - - new /obj/item/assembly/prox_sensor(Tsec) - new /obj/item/stack/rods(Tsec) - new /obj/item/stack/rods(Tsec) - new /obj/item/stack/cable_coil/cut(Tsec) - if(cell) - cell.forceMove(Tsec) - cell = null - - new /obj/effect/decal/cleanable/blood/oil(loc) - return ..() - -/mob/living/simple_animal/bot/mulebot/remove_air(amount) //To prevent riders suffocating - return loc ? loc.remove_air(amount) : null - -/mob/living/simple_animal/bot/mulebot/execute_resist() - . = ..() - if(load) - unload() - -/mob/living/simple_animal/bot/mulebot/UnarmedAttack(atom/A, proximity_flag, list/modifiers) - if(!can_unarmed_attack()) - return - if(isturf(A) && isturf(loc) && loc.Adjacent(A) && load) - unload(get_dir(loc, A)) - else - return ..() - -/// Checks whether the bot can complete a step_towards, checking whether the bot is on and has the charge to do the move. Returns COMPONENT_MOB_BOT_CANCELSTEP if the bot should not step. -/mob/living/simple_animal/bot/mulebot/proc/check_pre_step(datum/source) - SIGNAL_HANDLER - - if(!(bot_mode_flags & BOT_MODE_ON)) - return COMPONENT_MOB_BOT_BLOCK_PRE_STEP - - if((cell && (cell.charge < cell_move_power_usage)) || !has_power()) - turn_off() - return COMPONENT_MOB_BOT_BLOCK_PRE_STEP - -/// Uses power from the cell when the bot steps. -/mob/living/simple_animal/bot/mulebot/proc/on_bot_step(datum/source) - SIGNAL_HANDLER - - cell?.use(cell_move_power_usage) - -/mob/living/simple_animal/bot/mulebot/post_possession() - . = ..() - visible_message(span_notice("[src]'s safeties are locked on.")) - -/mob/living/simple_animal/bot/mulebot/paranormal//allows ghosts only unless hacked to actually be useful - name = "\improper GHOULbot" - desc = "A rather ghastly looking... Multiple Utility Load Effector bot? It only seems to accept paranormal forces, and for this reason is fucking useless." - icon_state = "paranormalmulebot0" - base_icon = "paranormalmulebot" - -/mob/living/simple_animal/bot/mulebot/paranormal/mouse_drop_receive(atom/movable/AM, mob/user, params) - var/mob/living/L = user - - if(user.incapacitated || (istype(L) && L.body_position == LYING_DOWN)) - return - - if(!istype(AM) || iseyemob(AM) || istype(AM, /obj/effect/dummy/phased_mob)) //allows ghosts! - return - - load(AM) - -/mob/living/simple_animal/bot/mulebot/paranormal/load(atom/movable/movable_atom) - if(load || movable_atom.anchored) - return - - if(!isturf(movable_atom.loc)) //To prevent the loading from stuff from someone's inventory or screen icons. - return - - if(isobserver(movable_atom)) - visible_message(span_warning("A ghostly figure appears on [src]!")) - movable_atom.forceMove(src) - RegisterSignal(movable_atom, COMSIG_MOVABLE_MOVED, PROC_REF(ghostmoved)) - - else if(!wires.is_cut(WIRE_LOADCHECK)) - buzz(SIGH) - return // if not hacked, only allow ghosts to be loaded - - else if(isobj(movable_atom)) - if(movable_atom.has_buckled_mobs() || (locate(/mob) in movable_atom)) //can't load non crates objects with mobs buckled to it or inside it. - buzz(SIGH) - return - - if(istype(movable_atom, /obj/structure/closet/crate)) - var/obj/structure/closet/crate/crate = movable_atom - crate.close() //make sure it's closed - - movable_atom.forceMove(src) - - else if(isliving(movable_atom) && !load_mob(movable_atom)) - return - - load = movable_atom - mode = BOT_IDLE - update_appearance() - -/mob/living/simple_animal/bot/mulebot/paranormal/update_overlays() - . = ..() - if(!isobserver(load)) - return - var/mutable_appearance/ghost_overlay = mutable_appearance('icons/mob/simple/mob.dmi', "ghost", layer + 0.01) //use a generic ghost icon, otherwise you can metagame who's dead if they have a custom ghost set - ghost_overlay.pixel_z = 12 - . += ghost_overlay - -/mob/living/simple_animal/bot/mulebot/paranormal/get_load_name() //Don't reveal the name of ghosts so we can't metagame who died and all that. - . = ..() - if(. && isobserver(load)) - return "Unknown" - -/mob/living/simple_animal/bot/mulebot/paranormal/proc/ghostmoved() - SIGNAL_HANDLER - visible_message(span_notice("The ghostly figure vanishes...")) - UnregisterSignal(load, COMSIG_MOVABLE_MOVED) - unload(0) - -#undef SIGH -#undef ANNOYED -#undef DELIGHT -#undef CHIME diff --git a/code/modules/mob/living/simple_animal/hostile/ooze.dm b/code/modules/mob/living/simple_animal/hostile/ooze.dm index 4bb2858bce17..749f60e58d20 100644 --- a/code/modules/mob/living/simple_animal/hostile/ooze.dm +++ b/code/modules/mob/living/simple_animal/hostile/ooze.dm @@ -11,7 +11,7 @@ emote_see = list("jiggles", "bounces in place") speak_emote = list("blorbles") atmos_requirements = null - hud_type = /datum/hud/ooze + hud_type = /datum/hud/living/ooze minbodytemp = 250 maxbodytemp = INFINITY faction = list(FACTION_SLIME) @@ -322,7 +322,7 @@ ///Ability that allows the owner to fire healing globules at mobs, targeting specific limbs. /datum/action/cooldown/globules name = "Fire Mending globule" - desc = "Fires a mending globule at someone, healing a specific limb of theirs." + desc = "Fires a mending globule at someone, healing a specific limb of theirs. Costs 5 nutrition." background_icon_state = "bg_hive" overlay_icon_state = "bg_hive_border" button_icon = 'icons/mob/actions/actions_slime.dmi' @@ -332,10 +332,16 @@ click_to_activate = TRUE /datum/action/cooldown/globules/set_click_ability(mob/on_who) + var/mob/living/simple_animal/hostile/ooze/oozy_owner = owner + if(istype(oozy_owner)) + if(oozy_owner.ooze_nutrition < 5) + to_chat(oozy_owner, span_warning("You need at least 5 nutrition to launch a mending globule.")) + return . = ..() if(!.) return + oozy_owner.adjust_ooze_nutrition(-5) to_chat(on_who, span_notice("You prepare to launch a mending globule. Left-click to fire at a target!")) /datum/action/cooldown/globules/unset_click_ability(mob/on_who, refund_cooldown = TRUE) @@ -344,21 +350,10 @@ return if(refund_cooldown) + var/mob/living/simple_animal/hostile/ooze/oozy_owner = owner + oozy_owner.adjust_ooze_nutrition(5) to_chat(on_who, span_notice("You stop preparing your mending globules.")) -/datum/action/cooldown/globules/Activate(atom/target) - . = ..() - if(!.) - return FALSE - - var/mob/living/simple_animal/hostile/ooze/oozy_owner = owner - if(istype(oozy_owner)) - if(oozy_owner.ooze_nutrition < 5) - to_chat(oozy_owner, span_warning("You need at least 5 nutrition to launch a mending globule.")) - return FALSE - - return TRUE - /datum/action/cooldown/globules/InterceptClickOn(mob/living/clicker, params, atom/target) . = ..() if(!.) @@ -426,7 +421,7 @@ ///This action lets you put a mob inside of a cacoon that will inject it with some chemicals. /datum/action/cooldown/gel_cocoon name = "Gel Cocoon" - desc = "Puts a mob inside of a cocoon, allowing it to slowly heal." + desc = "Puts a mob inside of a cocoon, allowing it to slowly heal. Costs 30 nutrition." background_icon_state = "bg_hive" overlay_icon_state = "bg_hive_border" button_icon = 'icons/mob/actions/actions_slime.dmi' diff --git a/code/modules/mod/modules/modules_antag.dm b/code/modules/mod/modules/modules_antag.dm index f7d28ed13410..e75565e43a91 100644 --- a/code/modules/mod/modules/modules_antag.dm +++ b/code/modules/mod/modules/modules_antag.dm @@ -540,11 +540,16 @@ /obj/item/mod/module/stealth/wraith/unstealth(datum/source) if(!stealth_active) return - . = ..() - if(mod.active) - COOLDOWN_START(src, recloak_timer, 20 SECONDS) - addtimer(CALLBACK(src, PROC_REF(start_stealth)), 20 SECONDS) - stealth_active = FALSE + to_chat(mod.wearer, span_warning("[src] gets discharged from contact!")) + do_sparks(2, TRUE, src) + drain_power(use_energy_cost) + // Don't deactivate() directly as the module may not be active in the first place when stealthing + on_deactivation() + if(!mod.active) + return + COOLDOWN_START(src, recloak_timer, 20 SECONDS) + addtimer(CALLBACK(src, PROC_REF(start_stealth)), 20 SECONDS) + stealth_active = FALSE /obj/item/mod/module/stealth/wraith/examine_more(mob/user) . = ..() diff --git a/code/modules/mod/modules/modules_ninja.dm b/code/modules/mod/modules/modules_ninja.dm index 04df80eb5408..1f530e8904d9 100644 --- a/code/modules/mod/modules/modules_ninja.dm +++ b/code/modules/mod/modules/modules_ninja.dm @@ -547,7 +547,7 @@ return COMPONENT_CANCEL_ATTACK_CHAIN //BOTS, overloads them and causes a explosion -/mob/living/simple_animal/bot/ninjadrain_act(mob/living/carbon/human/ninja, obj/item/mod/module/hacker/hacking_module) +/mob/living/basic/bot/ninjadrain_act(mob/living/carbon/human/ninja, obj/item/mod/module/hacker/hacking_module) to_chat(src, span_boldwarning("Your circutry suddenly begins heating up!")) if(!do_after(ninja, 1.5 SECONDS, target = src, hidden = TRUE)) return COMPONENT_CANCEL_ATTACK_CHAIN diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index f773dceb5262..7f92f7c63f58 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -154,6 +154,15 @@ install_default_programs() register_context() update_appearance() + if(mapload) + return INITIALIZE_HINT_LATELOAD + else + if(SStts.tts_enabled) + voice = SStts.computer_voice + +/obj/item/modular_computer/LateInitialize() + if(SStts.tts_enabled) + voice = SStts.computer_voice /obj/item/modular_computer/LateInitialize() . = ..() diff --git a/code/modules/modular_computers/file_system/programs/robocontrol.dm b/code/modules/modular_computers/file_system/programs/robocontrol.dm index 1176ce92114a..f7278fa3008e 100644 --- a/code/modules/modular_computers/file_system/programs/robocontrol.dm +++ b/code/modules/modular_computers/file_system/programs/robocontrol.dm @@ -32,32 +32,32 @@ botcount = 0 - for(var/mob/living/simple_animal/bot/simple_bot as anything in GLOB.bots_list) - if(!is_valid_z_level(current_turf, get_turf(simple_bot)) || !(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) //Only non-emagged bots on the same Z-level are detected! + for(var/mob/living/basic/bot/basic_bot as anything in GLOB.bots_list) + if(!is_valid_z_level(current_turf, get_turf(basic_bot)) || !(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) //Only non-emagged bots on the same Z-level are detected! continue - if(!simple_bot.allowed(user) && !simple_bot.check_access(computer.stored_id)) // Only check Bots we can access + if(!basic_bot.allowed(user) && !basic_bot.check_access(computer.stored_id)) // Only check Bots we can access continue var/list/newbot = list( - "name" = simple_bot.name, - "mode" = simple_bot.get_mode_ui(), - "model" = simple_bot.bot_type, - "locat" = get_area(simple_bot), - "bot_ref" = REF(simple_bot), + "name" = basic_bot.name, + "mode" = basic_bot.get_mode_ui(), + "model" = basic_bot.bot_type, + "locat" = get_area(basic_bot), + "bot_ref" = REF(basic_bot), "mule_check" = FALSE, ) - if(simple_bot.bot_type == MULE_BOT) - var/mob/living/simple_animal/bot/mulebot/simple_mulebot = simple_bot + if(basic_bot.bot_type == MULE_BOT) + var/mob/living/basic/bot/mulebot/basic_mulebot = basic_bot mulelist += list(list( - "name" = simple_mulebot.name, - "id" = simple_mulebot.id, - "dest" = simple_mulebot.destination, - "power" = simple_mulebot.cell ? simple_mulebot.cell.percent() : 0, - "home" = simple_mulebot.home_destination, - "autoReturn" = simple_mulebot.mulebot_delivery_flags & MULEBOT_RETURN_MODE, - "autoPickup" = simple_mulebot.mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE, - "reportDelivery" = simple_mulebot.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE, - "mule_ref" = REF(simple_mulebot), - "load" = simple_mulebot.get_load_name(), + "name" = basic_mulebot.name, + "id" = basic_mulebot.id, + "dest" = basic_mulebot.ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON], + "power" = basic_mulebot.cell ? basic_mulebot.cell.percent() : 0, + "home" = basic_mulebot.ai_controller.blackboard[BB_MULEBOT_HOME_BEACON], + "autoReturn" = basic_mulebot.mulebot_delivery_flags & MULEBOT_RETURN_MODE, + "autoPickup" = basic_mulebot.mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE, + "reportDelivery" = basic_mulebot.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE, + "mule_ref" = REF(basic_mulebot), + "load" = basic_mulebot.get_load_name(), )) newbot["mule_check"] = TRUE botlist += list(newbot) @@ -106,15 +106,15 @@ "report", "ejectpai", ) - var/mob/living/simple_animal/bot/simple_bot = locate(params["robot"]) in GLOB.bots_list + var/mob/living/basic/bot/basic_bot = locate(params["robot"]) in GLOB.bots_list if (action in standard_actions) - simple_bot.bot_control(action, current_user, id_card?.GetAccess()) + basic_bot.bot_control(action, current_user, id_card?.GetAccess()) if (action in MULE_actions) - simple_bot.bot_control(action, current_user, id_card?.GetAccess(), TRUE) + basic_bot.bot_control(action, current_user, id_card?.GetAccess(), TRUE) switch(action) if("summon") - simple_bot.bot_control(action, current_user, id_card ? id_card.access : id_card?.GetAccess()) + basic_bot.bot_control(action, current_user, id_card ? id_card.access : id_card?.GetAccess()) if("ejectcard") if(!computer || !computer.stored_id) return diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index d0f61730727d..51b6c3df9a57 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -413,7 +413,7 @@ if(!can_shoot()) //Just because you can pull the trigger doesn't mean it can shoot. shoot_with_empty_chamber(user) - return ITEM_INTERACT_BLOCKING + return user.combat_mode ? ITEM_INTERACT_SKIP_TO_ATTACK : ITEM_INTERACT_BLOCKING if(check_botched(user, target)) return NONE @@ -481,33 +481,30 @@ if(iteration > 1 && !(user.is_holding(src))) //for burst firing firing_burst = FALSE return FALSE - if(chambered?.loaded_projectile) - if(HAS_TRAIT(user, TRAIT_PACIFISM)) // If the user has the pacifist trait, then they won't be able to fire [src] if the round chambered inside of [src] is lethal. - if(chambered.harmful) // Is the bullet chambered harmful? - to_chat(user, span_warning("[src] is lethally chambered! You don't want to risk harming anyone...")) - firing_burst = FALSE - return FALSE - var/sprd - if(randomspread) - sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * (random_spread)) - else //Smart spread - sprd = round((((burst_spread_mult/burst_size) * iteration) - (0.5 + (burst_spread_mult * 0.25))) * (random_spread)) - before_firing(target,user) - if(!chambered.fire_casing(target, user, params, 0, suppressed, zone_override, sprd, src)) - shoot_with_empty_chamber(user) - firing_burst = FALSE - return FALSE - else - if(get_dist(user, target) <= 1) //Making sure whether the target is in vicinity for the pointblank shot - shoot_live_shot(user, TRUE, target, message) - else - shoot_live_shot(user, FALSE, target, message) - if (iteration >= burst_size) - firing_burst = FALSE - else + if(!chambered?.loaded_projectile) shoot_with_empty_chamber(user) firing_burst = FALSE return FALSE + + if(HAS_TRAIT(user, TRAIT_PACIFISM) && chambered.harmful) // Is the bullet chambered harmful? + to_chat(user, span_warning("[src] is lethally chambered! You don't want to risk harming anyone...")) + firing_burst = FALSE + return FALSE + + var/sprd = 0 + if(randomspread) + sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * (random_spread)) + else //Smart spread + sprd = round((((burst_spread_mult/burst_size) * iteration) - (0.5 + (burst_spread_mult * 0.25))) * (random_spread)) + before_firing(target,user) + if(!chambered.fire_casing(target, user, params, 0, suppressed, zone_override, sprd, src)) + shoot_with_empty_chamber(user) + firing_burst = FALSE + return FALSE + shoot_live_shot(user, get_dist(user, target) <= 1, target, message) + if (iteration >= burst_size) + firing_burst = FALSE + process_chamber() update_appearance() return TRUE @@ -579,25 +576,21 @@ for(var/i = 1 to burst_size) addtimer(CALLBACK(src, PROC_REF(process_burst), user, target, message, params, zone_override, total_random_spread, burst_spread_mult, i), modified_burst_delay * (i - 1)) addtimer(CALLBACK(src, PROC_REF(reset_fire_cd)), modified_fire_delay) // for the case of fire delay longer than burst + else - if(chambered) - if(HAS_TRAIT(user, TRAIT_PACIFISM)) // If the user has the pacifist trait, then they won't be able to fire [src] if the round chambered inside of [src] is lethal. - if(chambered.harmful) // Is the bullet chambered harmful? - to_chat(user, span_warning("[src] is lethally chambered! You don't want to risk harming anyone...")) - return NONE - var/sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * total_random_spread) - before_firing(target,user) - if(!chambered.fire_casing(target, user, params, 0, suppressed, zone_override, sprd, src)) - shoot_with_empty_chamber(user) - return NONE - else - if(get_dist(user, target) <= 1) //Making sure whether the target is in vicinity for the pointblank shot - shoot_live_shot(user, TRUE, target, message) - else - shoot_live_shot(user, FALSE, target, message) - else + if(!chambered) shoot_with_empty_chamber(user) + return user.combat_mode ? ITEM_INTERACT_SKIP_TO_ATTACK : NONE + if(HAS_TRAIT(user, TRAIT_PACIFISM) && chambered.harmful) // If the user has the pacifist trait, then they won't be able to fire [src] if the round chambered inside of [src] is lethal. + to_chat(user, span_warning("[src] is lethally chambered! You don't want to risk harming anyone...")) return NONE + var/sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * total_random_spread) + before_firing(target,user) + if(!chambered.fire_casing(target, user, params, 0, suppressed, zone_override, sprd, src)) + shoot_with_empty_chamber(user) + return user.combat_mode ? ITEM_INTERACT_SKIP_TO_ATTACK : NONE + + shoot_live_shot(user, get_dist(user, target) <= 1, target, message) // If gun gets destroyed as a result of firing if (!QDELETED(src)) process_chamber() @@ -605,10 +598,8 @@ fire_cd = TRUE addtimer(CALLBACK(src, PROC_REF(reset_fire_cd)), modified_fire_delay) - if(user) - user.update_held_items() + user?.update_held_items() SSblackbox.record_feedback("tally", "gun_fired", 1, type) - return TRUE /obj/item/gun/proc/reset_fire_cd() @@ -698,7 +689,7 @@ target.visible_message(span_warning("[user] pulls the trigger!"), span_userdanger("[(user == target) ? "You pull" : "[user] pulls"] the trigger!")) if(!chambered?.loaded_projectile) - shoot_with_empty_chamber() + shoot_with_empty_chamber(user) return ITEM_INTERACT_BLOCKING chambered.loaded_projectile.damage *= 5 diff --git a/code/modules/projectiles/guns/magic.dm b/code/modules/projectiles/guns/magic.dm index f32d53cae10d..d3c85f0f1e04 100644 --- a/code/modules/projectiles/guns/magic.dm +++ b/code/modules/projectiles/guns/magic.dm @@ -144,7 +144,7 @@ recharge_newshot() return 1 -/obj/item/gun/magic/shoot_with_empty_chamber(mob/living/user as mob|obj) +/obj/item/gun/magic/shoot_with_empty_chamber(mob/living/user) to_chat(user, span_warning("\The [src] whizzles quietly.")) /obj/item/gun/magic/suicide_act(mob/living/user) diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm index 4f755474024d..c3be5f228ec3 100644 --- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm @@ -555,7 +555,7 @@ name = "Gin and Tonic" description = "An all time classic, mild cocktail." color = "#cae7ec" // rgb: 202,231,236 - boozepwr = 25 + boozepwr = 20 quality = DRINK_NICE taste_description = "mild and tart" ph = 3 @@ -567,7 +567,7 @@ name = "Rum and Coke" description = "Rum, mixed with cola." taste_description = "cola" - boozepwr = 40 + boozepwr = 30 quality = DRINK_NICE color = "#3E1B00" ph = 4 @@ -599,7 +599,7 @@ name = "Whiskey Cola" description = "Whiskey, mixed with cola. Surprisingly refreshing." color = "#3E1B00" // rgb: 62, 27, 0 - boozepwr = 70 + boozepwr = 40 quality = DRINK_NICE taste_description = "cola" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -641,7 +641,7 @@ name = "Screwdriver" description = "Vodka, mixed with plain ol' orange juice. The result is surprisingly delicious." color = "#A68310" // rgb: 166, 131, 16 - boozepwr = 55 + boozepwr = 40 quality = DRINK_NICE taste_description = "oranges" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -695,6 +695,7 @@ description = "Ewww..." color = "#8CFF8C" // rgb: 140, 255, 140 boozepwr = 45 + quality = DRINK_GOOD taste_description = "sweet 'n creamy" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS @@ -836,7 +837,7 @@ name = "Irish Cream" description = "Whiskey-imbued cream, what else would you expect from the Irish?" color = "#e3d0b2" // rgb: 227,208,178 - boozepwr = 50 + boozepwr = 35 quality = DRINK_NICE taste_description = "creamy alcohol" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -875,7 +876,7 @@ name = "Long Island Iced Tea" description = "The liquor cabinet, brought together in a delicious mix. Intended for middle-aged alcoholic women only." color = "#ff6633" // rgb: 255,102,51 - boozepwr = 35 + boozepwr = 50 quality = DRINK_VERYGOOD taste_description = "a mixture of cola and alcohol" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -909,7 +910,7 @@ name = "Irish Coffee" description = "Coffee, and alcohol. More fun than a Mimosa to drink in the morning." color = "#874010" // rgb: 135,64,16 - boozepwr = 35 + boozepwr = 30 quality = DRINK_NICE taste_description = "giving up on the day" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -931,7 +932,7 @@ name = "Black Russian" description = "For the lactose-intolerant. Still as classy as a White Russian." color = "#360000" // rgb: 54, 0, 0 - boozepwr = 70 + boozepwr = 60 quality = DRINK_NICE taste_description = "bitterness" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -941,7 +942,7 @@ name = "Manhattan" description = "The Detective's undercover drink of choice. He never could stomach gin..." color = "#ff3300" // rgb: 255,51,0 - boozepwr = 30 + boozepwr = 50 quality = DRINK_NICE taste_description = "mild dryness" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -952,7 +953,7 @@ name = "Manhattan Project" description = "A scientist's drink of choice, for pondering ways to blow up the station." color = COLOR_MOSTLY_PURE_RED - boozepwr = 45 + boozepwr = 60 quality = DRINK_VERYGOOD taste_description = "death, the destroyer of worlds" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -966,7 +967,7 @@ name = "Whiskey Soda" description = "For the more refined griffon." color = "#ffcc33" // rgb: 255,204,51 - boozepwr = 70 + boozepwr = 40 quality = DRINK_NICE taste_description = "soda" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -990,7 +991,7 @@ name = "Barefoot" description = "Barefoot and pregnant." color = "#fc5acc" // rgb: 252,90,204 - boozepwr = 45 + boozepwr = 30 quality = DRINK_VERYGOOD taste_description = "creamy berries" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1008,7 +1009,7 @@ name = "Snow White" description = "A cold refreshment." color = COLOR_WHITE // rgb: 255, 255, 255 - boozepwr = 35 + boozepwr = 20 quality = DRINK_NICE taste_description = "refreshing cold" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1101,7 +1102,7 @@ name = "Vodka and Tonic" description = "For when a gin and tonic isn't Russian enough." color = "#0064C8" // rgb: 0, 100, 200 - boozepwr = 70 + boozepwr = 40 quality = DRINK_NICE taste_description = "tart bitterness" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1111,7 +1112,7 @@ name = "Gin Fizz" description = "Refreshingly lemony, deliciously dry." color = "#ffffcc" // rgb: 255,255,204 - boozepwr = 45 + boozepwr = 25 quality = DRINK_GOOD taste_description = "dry, tart lemons" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1222,7 +1223,7 @@ name = "Aloe" description = "So very, very, very good." color = "#f8f800" // rgb: 248,248,0 - boozepwr = 35 + boozepwr = 30 quality = DRINK_VERYGOOD taste_description = "sweet 'n creamy" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1234,7 +1235,7 @@ name = "Andalusia" description = "A nice, strangely named drink." color = "#c8f860" // rgb: 200,248,96 - boozepwr = 40 + boozepwr = 45 quality = DRINK_GOOD taste_description = "lemons" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1244,7 +1245,7 @@ name = "Allies Cocktail" description = "A drink made from your allies. Not as sweet as those made from your enemies." color = "#60f8f8" // rgb: 96,248,248 - boozepwr = 45 + boozepwr = 50 quality = DRINK_NICE taste_description = "bitter yet free" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1265,7 +1266,7 @@ name = "Amasec" description = "Official drink of the Nanotrasen Gun-Club!" color = "#e0e058" // rgb: 224,224,88 - boozepwr = 35 + boozepwr = 45 quality = DRINK_GOOD taste_description = "dark and metallic" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1384,7 +1385,7 @@ name = "Drunken Blumpkin" description = "A weird mix of whiskey and blumpkin juice." color = "#1EA0FF" // rgb: 30,160,255 - boozepwr = 50 + boozepwr = 30 quality = DRINK_VERYGOOD taste_description = "molasses and a mouthful of pool water" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1717,7 +1718,7 @@ name = "Grasshopper" description = "A fresh and sweet dessert shooter. Difficult to look manly while drinking this." color = "#00ff00" - boozepwr = 25 + boozepwr = 15 quality = DRINK_GOOD taste_description = "chocolate and mint dancing around your mouth" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1727,7 +1728,7 @@ name = "Stinger" description = "A snappy way to end the day." color = "#ccff99" - boozepwr = 25 + boozepwr = 55 quality = DRINK_NICE taste_description = "a slap on the face in the best possible way" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1952,7 +1953,7 @@ name = "Kamikaze" description = "Divinely windy." color = "#EEF191" - boozepwr = 60 + boozepwr = 35 quality = DRINK_GOOD taste_description = "divine windiness" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -1962,7 +1963,7 @@ name = "Mojito" description = "A drink that looks as refreshing as it tastes." color = "#DFFAD9" - boozepwr = 30 + boozepwr = 20 quality = DRINK_GOOD taste_description = "refreshing mint" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -2216,7 +2217,7 @@ name = "Bug Spray" description = "A harsh, acrid, bitter drink, for those who need something to brace themselves." color = "#33ff33" - boozepwr = 50 + boozepwr = 45 quality = DRINK_GOOD taste_description = "the pain of ten thousand slain mosquitos" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED @@ -2501,7 +2502,7 @@ /datum/reagent/consumable/ethanol/white_tiziran name = "White Tiziran" description = "A mix of vodka and kortara. The Lizard imbibes." - boozepwr = 65 + boozepwr = 50 color = "#A68340" quality = DRINK_GOOD taste_description = "strikes and gutters" @@ -2652,7 +2653,7 @@ /datum/reagent/consumable/ethanol/admiralty //navy rum, vermouth, fernet name = "Admiralty" description = "A refined, bitter drink made with navy rum, vermouth and fernet." - boozepwr = 100 + boozepwr = 80 color = "#1F0001" quality = DRINK_VERYGOOD taste_description = "haughty arrogance" @@ -2662,7 +2663,7 @@ /datum/reagent/consumable/ethanol/long_haul //Rum, Curacao, Sugar, dash of bitters, lengthened with soda water name = "Long Haul" description = "A favourite amongst freighter pilots, unscrupulous smugglers, and nerf herders." - boozepwr = 35 + boozepwr = 20 color = "#003153" quality = DRINK_VERYGOOD taste_description = "companionship" @@ -2672,7 +2673,7 @@ /datum/reagent/consumable/ethanol/long_john_silver //navy rum, bitters, lemonade name = "Long John Silver" description = "A long drink of navy rum, bitters, and lemonade. Particularly popular aboard the Mothic Fleet as it's light on ration credits and heavy on flavour." - boozepwr = 50 + boozepwr = 45 color = "#c4b35c" quality = DRINK_VERYGOOD taste_description = "rum and spices" @@ -2682,7 +2683,7 @@ /datum/reagent/consumable/ethanol/tropical_storm //dark rum, pineapple juice, triple citrus, curacao name = "Tropical Storm" description = "A taste of the Caribbean in one glass." - boozepwr = 40 + boozepwr = 25 color = "#00bfa3" quality = DRINK_VERYGOOD taste_description = "the tropics" @@ -2692,7 +2693,7 @@ /datum/reagent/consumable/ethanol/dark_and_stormy //rum and ginger beer- simple and classic name = "Dark and Stormy" description = "A classic drink arriving to thunderous applause." //thank you, thank you, I'll be here forever - boozepwr = 50 + boozepwr = 30 color = "#8c5046" quality = DRINK_GOOD taste_description = "ginger and rum" @@ -3150,7 +3151,7 @@ /datum/reagent/consumable/ethanol/french_75 name = "French 75" description = "A sophisticated cocktail made by strengthening champagne with gin, then flavoring with lemon juice and sugar." - boozepwr = 30 + boozepwr = 35 color = "#ffffc1" quality = DRINK_GOOD taste_description = "glory and gunnery" @@ -3315,7 +3316,7 @@ /datum/reagent/consumable/ethanol/casino name = "Casino" description = "A gin-heavy classic. Juniper is tempered by scant amounts of citrus and sweetened with liqueur." - boozepwr = 50 + boozepwr = 45 color = "#fdee65" quality = DRINK_GOOD taste_description = "sweet juniper" diff --git a/code/modules/reagents/reagent_containers/rag.dm b/code/modules/reagents/reagent_containers/rag.dm index 639e5e1d7ac7..f0c1e579a622 100644 --- a/code/modules/reagents/reagent_containers/rag.dm +++ b/code/modules/reagents/reagent_containers/rag.dm @@ -130,7 +130,8 @@ if(isturf(clean_target) && !HAS_TRAIT(cleaned, TRAIT_MOPABLE)) continue // collect dna FIRST - all_blood_dna |= all_cleaned[cleaned] + if (!isnull(all_cleaned[cleaned])) // Check if there was any blood on it, we don't want nulls in our list + all_blood_dna |= all_cleaned[cleaned] // THEN pass on dna (though in some cases the cleaned item is being deleted) if(blood_level > 0 && !QDELING(cleaned)) cleaned.add_blood_DNA(GET_ATOM_BLOOD_DNA(src)) diff --git a/code/modules/recycling/disposal/bin.dm b/code/modules/recycling/disposal/bin.dm index dfbed0e37f5e..002b77eee1f7 100644 --- a/code/modules/recycling/disposal/bin.dm +++ b/code/modules/recycling/disposal/bin.dm @@ -438,7 +438,8 @@ GLOBAL_VAR_INIT(disposals_animals_spawned, 0) var/obj/item/dest_tagger/mounted_tagger ///a weighted list of all the possible animals we can have var/static/list/weighted_animal_list = list( - /mob/living/basic/stoat = 1, + /mob/living/basic/stoat = 5, + /mob/living/basic/stoat/kit = 1, ) /// do we contain an animal? var/contained_animal diff --git a/code/modules/research/designs/autolathe/materials.dm b/code/modules/research/designs/autolathe/materials.dm index 8fdc01234ce2..2027882e5244 100644 --- a/code/modules/research/designs/autolathe/materials.dm +++ b/code/modules/research/designs/autolathe/materials.dm @@ -1,165 +1,22 @@ -/datum/design/iron - name = "Iron" - id = "iron" - build_type = AUTOLATHE - materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/iron - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - /datum/design/rods name = "Iron Rod" id = "rods" build_type = AUTOLATHE - materials = list(/datum/material/iron =HALF_SHEET_MATERIAL_AMOUNT) + materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT) build_path = /obj/item/stack/rods category = list( RND_CATEGORY_INITIAL, RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, ) -/datum/design/glass - name = "Glass" - id = "glass" - build_type = AUTOLATHE - materials = list(/datum/material/glass = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/glass - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - /datum/design/rglass name = "Reinforced Glass" id = "rglass" build_type = AUTOLATHE | SMELTER | PROTOLATHE | AWAY_LATHE - materials = list(/datum/material/iron =HALF_SHEET_MATERIAL_AMOUNT, /datum/material/glass = SHEET_MATERIAL_AMOUNT) + materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/glass = SHEET_MATERIAL_AMOUNT) build_path = /obj/item/stack/sheet/rglass category = list( RND_CATEGORY_INITIAL, RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, ) departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_SCIENCE - -/datum/design/silver - name = "Silver" - id = "silver" - build_type = AUTOLATHE - materials = list(/datum/material/silver = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/silver - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/gold - name = "Gold" - id = "gold" - build_type = AUTOLATHE - materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/gold - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/diamond - name = "Diamond" - id = "diamond" - build_type = AUTOLATHE - materials = list(/datum/material/diamond = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/diamond - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/plasma - name = "Plasma" - id = "plasma" - build_type = AUTOLATHE - materials = list(/datum/material/plasma = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/plasma - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/uranium - name = "Uranium" - id = "uranium" - build_type = AUTOLATHE - materials = list(/datum/material/uranium = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/uranium - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/bananium - name = "Bananium" - id = "bananium" - build_type = AUTOLATHE - materials = list(/datum/material/bananium = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/bananium - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/titanium - name = "Titanium" - id = "titanium" - build_type = AUTOLATHE - materials = list(/datum/material/titanium = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/titanium - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/plastic - name = "Plastic" - id = "plastic" - build_type = AUTOLATHE - materials = list(/datum/material/plastic = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/plastic - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/bs_crystal - name = "Bluespace Crystal" - id = "bscrystal" - build_type = AUTOLATHE - materials = list(/datum/material/bluespace = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/bluespace_crystal - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/mythril - name = "Mythril" - id = "mythril" - build_type = AUTOLATHE - materials = list(/datum/material/mythril = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/mythril - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) - -/datum/design/alien_alloy - name = "Alien Alloy" - id = "allienalloy" - build_type = AUTOLATHE - materials = list(/datum/material/alloy/alien = SHEET_MATERIAL_AMOUNT) - build_path = /obj/item/stack/sheet/mineral/abductor - category = list( - RND_CATEGORY_INITIAL, - RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS, - ) diff --git a/code/modules/research/gizmo/gizmo_controller.dm b/code/modules/research/gizmo/gizmo_controller.dm index 90b5f7bef879..a1bc793529f0 100644 --- a/code/modules/research/gizmo/gizmo_controller.dm +++ b/code/modules/research/gizmo/gizmo_controller.dm @@ -40,3 +40,7 @@ /// Always horrible agony! /datum/gizmo_controller/cursed interfaces = list(GIZMO_INTERFACE_WIRES = /datum/gizmo_interface/cursed) + +/// Hard-mode code-crack +/datum/gizmo_controller/moo + interfaces = list(GIZMO_INTERFACE_WIRES = /datum/gizmo_interface/moo) diff --git a/code/modules/research/gizmo/gizmo_interface.dm b/code/modules/research/gizmo/gizmo_interface.dm index 7e46a47fcc64..39e23ec6e033 100644 --- a/code/modules/research/gizmo/gizmo_interface.dm +++ b/code/modules/research/gizmo/gizmo_interface.dm @@ -67,3 +67,8 @@ /datum/gizmodes/bad = 1, ) max_modes = 1 + +/datum/gizmo_interface/moo + guaranteed_active_gizmodes = list(/datum/gizmodes/code_crack/moo) + min_modes = 0 + max_modes = 0 diff --git a/code/modules/research/gizmo/gizmo_items.dm b/code/modules/research/gizmo/gizmo_items.dm index bc6d237681a1..1e18242556e5 100644 --- a/code/modules/research/gizmo/gizmo_items.dm +++ b/code/modules/research/gizmo/gizmo_items.dm @@ -20,3 +20,15 @@ controller = new controller(src) controller.generate_interfaces(src) + +/obj/item/gizmo/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + add_fingerprint(user) + + if(is_wire_tool(tool)) + attempt_wire_interaction(user) + return ITEM_INTERACT_SUCCESS + return NONE + +/// Hard-mode code-crack gizmo +/obj/item/gizmo/moo + controller = /datum/gizmo_controller/moo diff --git a/code/modules/research/gizmo/gizmo_machines.dm b/code/modules/research/gizmo/gizmo_machines.dm index 3d50d797d5d7..ed79b912a421 100644 --- a/code/modules/research/gizmo/gizmo_machines.dm +++ b/code/modules/research/gizmo/gizmo_machines.dm @@ -120,13 +120,15 @@ SIGNAL_HANDLER on_state = TRUE - update_icon() + update_appearance(UPDATE_ICON) + visible_message(span_smallnoticeital("[src] hums to life.")) /obj/machinery/gizmo/toggle/proc/off_state(datum/source) SIGNAL_HANDLER on_state = FALSE - update_icon() + update_appearance(UPDATE_ICON) + visible_message(span_smallnoticeital("[src] powers down.")) /// A gizmo with a voice activated interface /obj/machinery/gizmo/voice diff --git a/code/modules/research/gizmo/gizmodes/gizcode.dm b/code/modules/research/gizmo/gizmodes/gizcode.dm new file mode 100644 index 000000000000..51249c06aa94 --- /dev/null +++ b/code/modules/research/gizmo/gizmodes/gizcode.dm @@ -0,0 +1,315 @@ +// How many times we try to generate the code +#define MAX_CODEGEN_RETRY_ATTEMPTS 5 +// There are 10 digits +#define DIGIT_COUNT 10 +/** A code-cracking puzzle gizmo + * First, the user must pulse activate_puzzle - this randomizes the code and resets the attempt counter. + * The user may pulse activate_puzzle a limited amount of times again to retry it. + * + * Next, the user needs to input a code. + * Imagine a row of knobs, each of them with 10 notches, and a head that rotates the knobs. + * Pulsing cycle_position moves the head right, + * pulsing cycle_digit makes the head bump the knob by 1 notch. + * If the head reaches the rightmost position, then cycle_position resets it, moving it back left. + * Similarly, pulsing cycle_digit when the knob is already in the rightmost notch resets it to the leftmost one. + * + * Finally, the user has to pulse try_crack to check if their input is correct. + * If it's correct, a reward is dispensed and the puzzle gets deactivated. + * If it's not, some sort of feedback is provided. + * If the user takes too many attempts to solve the code, they are punished. + * + * There may be restrictions set upon the code. + * These are checked upon code generation and cracking attempt. + */ +/datum/gizmodes/code_crack + abstract_type = /datum/gizmodes/code_crack + guaranteed_active_gizmodes = list( + /datum/gizpulse/activate_puzzle, + /datum/gizpulse/cycle_position, + /datum/gizpulse/cycle_digit, + /datum/gizpulse/try_crack, + ) + + mode_pulses = list( + /datum/gizpulse/mode_controle/direct_activate, + ) + // Sound that plays upon puzzle activation + var/init_jingle = "sound/machines/terminal/terminal_processing.ogg" + // How many times you can activate the code-cracking puzzle + var/puzzles_left = 3 + // How many attempts the user has to crack the code before the gizmo starts punishing them + var/attempts_left = 10 + + // Whether the puzzle is currently active + var/active = FALSE + // Code length + var/code_length = 2 + // Solution to current puzzle + var/list/solution = list(0, 0, 0, 0,) + // Current code cracking input + var/list/code_input = list(0, 0, 0, 0,) + // Which position in the code is currently being cycled + var/position = 0 + + var/list/loot_table = null + +// Proc that generates the code (solution to the puzzle) +// We retry generation if the code happened to be invalid +// Returns TRUE if generation was successful and FALSE otherwise +/datum/gizmodes/code_crack/proc/generate_code() + SHOULD_CALL_PARENT(TRUE) + solution.Cut() + solution.len = code_length + for(var/i in 1 to MAX_CODEGEN_RETRY_ATTEMPTS) + for(var/j in 1 to code_length) + solution[j] = rand(0, 9) // Randomize every digit + if(validate_code(solution)) + return TRUE + return FALSE + +// Proc that checks if code is valid or not (matches the restrictions) +/datum/gizmodes/code_crack/proc/validate_code(list/code) + // Restrictions are defined by the subtype + return TRUE + +// Proc that checks if user input matches the solution +/datum/gizmodes/code_crack/proc/check_code() + for(var/i in 1 to code_length) + if(code_input[i] != solution[i]) + return FALSE + return TRUE + +// Proc to dispense the reward from the loot table +/datum/gizmodes/code_crack/proc/dispense_reward(atom/movable/holder) + SHOULD_CALL_PARENT(TRUE) + var/loot = pick_weight_recursive(loot_table) + new loot(get_turf(holder)) + playsound(holder,"sound/machines/machine_vend.ogg", 100) + +// Proc that punishes the user when they go over the attempt limit +// Technically, user can try to crack the code as many times as they want, as long as they can endure the punishment +/datum/gizmodes/code_crack/proc/punishment(atom/movable/holder) + // Punishment has to be defined by the subtype + return + +// Proc that produces feedback when the user inputs an incorrect code +// By default, all of these gizmos tell the user how many attempts are left +/datum/gizmodes/code_crack/proc/feedback(atom/movable/holder) + SHOULD_CALL_PARENT(TRUE) + // This is kind of ass, but there's probably no way around it + var/static/list/digit_to_name = list("one", "two", "three", "four", "five", "six", "seven", "eight", "nine") + if(attempts_left <= 0 || attempts_left >= 10) + return + playsound(holder, "sound/announcer/vox_fem/[digit_to_name[attempts_left]].ogg", 100) + holder.say(digit_to_name[attempts_left]) + +// Proc that resets user input +/datum/gizmodes/code_crack/proc/reset_input() + code_input.Cut() + code_input.len = code_length // stretch it + for(var/i in 1 to code_length) + code_input[i] = 0 // Fill it with zeroes + position = initial(position) + +// Gizpulses + +// Gizpulse to activate the puzzle +/datum/gizpulse/activate_puzzle/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface) + var/datum/gizmodes/code_crack/puzzle_holder = astype(master) + if(!puzzle_holder) + return + // If the puzzle cannot be retried, produce a bad buzz and stop + if(puzzle_holder.puzzles_left <= 0) + playsound(holder, "sound/machines/uplink/uplinkerror.ogg", 100) + return + // If it can be tried again (or is launched for the first time), do what we gotta do + if(!puzzle_holder.generate_code()) // Code generation may fail, if the restrictions are too severe + playsound(holder, "sound/items/ceramic_break.ogg", 100) + return + playsound(holder, puzzle_holder.init_jingle, 100) + puzzle_holder.puzzles_left-- + puzzle_holder.active = TRUE + puzzle_holder.reset_input() + puzzle_holder.attempts_left = initial(puzzle_holder.attempts_left) + +// Gizpulse to cycle the currently selected position +// Example (if code_input is 0000): +// 0000 -> 0000 +// ^ ^ +/datum/gizpulse/cycle_position/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface) + var/datum/gizmodes/code_crack/puzzle_holder = astype(master) + if(!puzzle_holder) + return + if(!puzzle_holder.active) // If the puzzle is inactive, produce a loud buzz and get out + playsound(holder,"sound/machines/scanner/scanbuzz.ogg", 100) + return + // Cycle position: 0 -> 1 -> 2 -> .. -> (code_length - 1) -> reset back to 0 + puzzle_holder.position = (puzzle_holder.position + 1) % puzzle_holder.code_length + + // If we simply bumped the position by 1, produce a single piston-move sound + if(puzzle_holder.position != 0) + playsound(holder, "sound/machines/eject.ogg", 100) + return + // Otherwise, produce a different sound, indicating the position has been reset + playsound(holder, "sound/items/weapons/autoguninsert.ogg", 100) + +// Gizpulse to cycle the currently selected digit +// Example (if second digit is selected and code_input is 0000): +// 0000 -> 0100 +// ^ ^ +/datum/gizpulse/cycle_digit/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface) + var/datum/gizmodes/code_crack/puzzle_holder = astype(master) + if(!puzzle_holder) + return + if(!puzzle_holder.active) // If the puzzle is inactive, produce a loud buzz and get out + playsound(holder,"sound/machines/scanner/scanbuzz.ogg", 100) + return + + // List indices start with 1, so we add 1 here + var/position = puzzle_holder.position + 1 + var/previous_digit = puzzle_holder.code_input[position] + // Cycle the digit + puzzle_holder.code_input[position] = (previous_digit + 1) % DIGIT_COUNT + + // If we simply bumped the digit by 1, produce a single click + if(previous_digit != DIGIT_COUNT - 1) + playsound(holder, "sound/machines/creak.ogg", 100) + return + // Otherwise, produce a different sound, indicating the digit has been reset + playsound(holder, "sound/items/reel/reel4.ogg", 100) + +// Gizpulse that actually cracks the code +/datum/gizpulse/try_crack/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface) + var/datum/gizmodes/code_crack/puzzle_holder = astype(master) + if(!puzzle_holder) + return + if(!puzzle_holder.active) // If the puzzle is inactive, produce a loud buzz and get out + playsound(holder,"sound/machines/scanner/scanbuzz.ogg", 100) + return + + // If the input is invalid, emit an invalid-input sound and let the user make corrections + var/validity = puzzle_holder.validate_code(puzzle_holder.code_input) + if(!validity) + playsound(holder, "sound/machines/terminal/terminal_error.ogg", 100) + return + + // If the input is correct, dispense a reward and reset the puzzle + var/correctness = puzzle_holder.check_code() + if(correctness) + puzzle_holder.dispense_reward(holder) + puzzle_holder.active = FALSE + return + + // If the input is incorrect.. + puzzle_holder.attempts_left-- + if(puzzle_holder.attempts_left <= 0) + puzzle_holder.punishment(holder) + puzzle_holder.feedback(holder) + puzzle_holder.reset_input() + // Play the input reset sound + playsound(holder, "sound/machines/terminal/terminal_eject.ogg", 100) + +// Tutorial version +// Restrictions: none +// Code length: 2 +// Feedback: over/under +// Punishment: evil rat +// Loot: cheese +// Also, dispenses a hard-mode code-crack gizmo upon completion +/datum/gizmodes/code_crack/tutorial + loot_table = list( + list( // Cheese slices + /obj/item/food/cheese/firm_cheese_slice = 1, + /obj/item/food/cheese/wedge = 1, + ) = 39, + /obj/item/food/cheese/wheel = 20, // Normal cheese + list( // Firm cheese + /obj/item/food/cheese/curd_cheese = 1, + /obj/item/food/cheese/cheese_curds = 1, + /obj/item/food/cheese/firm_cheese = 1, + ) = 20, + /obj/item/food/cheese/mozzarella = 20, // Mozzarella + /obj/item/food/cheese/royal = 1, // Royal + ) + var/dispensed_hardmode = FALSE + +/datum/gizmodes/code_crack/tutorial/dispense_reward(atom/movable/holder) + if(!dispensed_hardmode) + dispensed_hardmode = TRUE + // Hard-mode + new /obj/item/gizmo/moo(get_turf(holder)) + ..() + +/datum/gizmodes/code_crack/tutorial/feedback(atom/movable/holder) + for(var/i in 1 to code_length) + if(code_input[i] < solution[i]) + playsound(holder, "sound/machines/defib/defib_saftyOff.ogg", 100) + holder.visible_message(span_notice("[holder] pings low.")) + else if(code_input[i] > solution[i]) + playsound(holder, "sound/machines/defib/defib_saftyOn.ogg", 100) + holder.visible_message(span_notice("[holder] pings high.")) + else + playsound(holder, "sound/machines/defib/defib_ready.ogg", 100) + holder.visible_message(span_notice("[holder] pings affirmatively.")) + sleep(0.5 SECONDS) + ..() + +/datum/gizmodes/code_crack/tutorial/punishment(atom/movable/holder) + // Evil rat + new /mob/living/basic/mouse/rat(get_turf(holder)) + +// Hardmode +// Restrictions: all digits must be unique +// Code length: 2-3 +// Feedback: bulls and cows (number of correctly placed digits, number of incorrectly placed digits that are included in the code) +// Punishment: explosion +// Loot: all sorts of stuff +/datum/gizmodes/code_crack/moo + // Moo + init_jingle = "sound/mobs/non-humanoids/cow/cow.ogg" + loot_table = /obj/structure/closet/crate/secure/loot::possible_loot + var/min_code_length = 2 + var/max_code_length = 3 + +// All digits must be unique +/datum/gizmodes/code_crack/moo/validate_code(code) + for(var/i in 1 to code_length) + for(var/j in 1 to i-1) + if(code[i] == code[j]) + return FALSE + return TRUE + +/datum/gizmodes/code_crack/moo/generate_code() + code_length = rand(min_code_length, max_code_length) + return ..() + +/datum/gizmodes/code_crack/moo/feedback(atom/movable/holder) + var/bulls = 0 + var/cows = 0 + for(var/i in 1 to code_length) + for(var/j in 1 to code_length) + if(code_input[i] == solution[j]) + if(i == j) // Digit is correct and correctly placed + bulls++ + else // Digit is correct, but incorrectly placed + cows++ + break + // Bull beeps, cow beeps and the sound from parent call shouldn't play simultaneously, so sleep() is probably unavoidable here + for(var/i in 1 to bulls) + playsound(holder, "sound/machines/synth/synth_yes.ogg", 100) + sleep(0.25 SECONDS) + for(var/i in 1 to cows) + playsound(holder, "sound/machines/synth/synth_no.ogg", 100) + sleep(0.25 SECONDS) + + holder.visible_message(span_notice("[holder] emits [bulls] high-pitched beeps and [cows] low-pitched ones.")) + + ..() + +/datum/gizmodes/code_crack/moo/punishment(atom/movable/holder) + var/obj/item/grenade/syndieminibomb/punishment = new(get_turf(holder)) + punishment.arm_grenade(null, 5 SECONDS) + qdel(holder) + +#undef MAX_CODEGEN_RETRY_ATTEMPTS +#undef DIGIT_COUNT diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index 79a47f4d2440..3def867181e2 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -1045,6 +1045,11 @@ GLOBAL_LIST_INIT(slime_extract_auto_activate_reactions, init_slime_auto_activate to_chat(user, span_warning("The potion can only be used on gendered things!")) return ITEM_INTERACT_BLOCKING + if(living_mob.mind) + if (!do_after(user, delay = 5 SECONDS, target = living_mob)) + balloon_alert(user, "interrupted!") + return ITEM_INTERACT_BLOCKING + if(living_mob.gender == MALE) living_mob.gender = FEMALE living_mob.visible_message(span_boldnotice("[living_mob] suddenly looks more feminine!"), span_boldwarning("You suddenly feel more feminine!")) diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm index 7ecd0e6b8525..c9b84527ebff 100644 --- a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm +++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm @@ -165,9 +165,9 @@ new_shape.add_traits(list(TRAIT_DONT_WRITE_MEMORY, TRAIT_SHAPESHIFTED), SHAPESHIFT_TRAIT) // Make sure that if you shapechanged into a bot, the AI can't just turn you off. - var/mob/living/simple_animal/bot/polymorph_bot = new_shape + var/mob/living/basic/bot/polymorph_bot = new_shape if (istype(polymorph_bot)) - polymorph_bot.bot_cover_flags |= BOT_COVER_EMAGGED + polymorph_bot.bot_access_flags |= BOT_COVER_EMAGGED polymorph_bot.bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED return new_shape diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index ecb3ad626a8e..8b1a366c0a05 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -103,6 +103,10 @@ analyze_telemetry(payload) return TRUE + if(type == "requestMetadata") + send_metadata() + return TRUE + /** * public * @@ -110,3 +114,32 @@ */ /datum/tgui_panel/proc/send_roundrestart() window.send_message("roundrestart") + +/** + * private + * + * Sent when a client requests metadata - used for websocket stuff. + */ +/datum/tgui_panel/proc/send_metadata() + var/static/list/webroot_asset_urls + + var/list/metadata = list( + "game_version" = GLOB.game_version, + "server_name" = CONFIG_GET(string/servername), + "round_id" = GLOB.round_id, + "map_name" = SSmapping.current_map?.map_name, + "round_duration" = round(STATION_TIME_PASSED() / 10, 1), + "gamestate" = SSticker.current_state, + ) + // if we're using webroot - also pass along the webroot url and such, so we can embed chat logs with the proper styles/images if desired + if(istype(SSassets.transport, /datum/asset_transport/webroot)) + if(isnull(webroot_asset_urls)) + webroot_asset_urls = list() + for(var/asset_type in list(/datum/asset/simple/tgui_panel, /datum/asset/simple/namespaced/fontawesome, /datum/asset/simple/namespaced/tgfont, /datum/asset/spritesheet_batched/chat)) + var/datum/asset/asset = get_asset_datum(asset_type) + webroot_asset_urls += asset.get_url_mappings() + metadata["webroot"] = list( + "base_url" = CONFIG_GET(string/asset_cdn_url), + "assets" = webroot_asset_urls, + ) + window.send_message("metadata", metadata) diff --git a/code/modules/transport/tram/tram_controller.dm b/code/modules/transport/tram/tram_controller.dm index 38cf50d0ef7f..e9a264b51902 100644 --- a/code/modules/transport/tram/tram_controller.dm +++ b/code/modules/transport/tram/tram_controller.dm @@ -378,19 +378,20 @@ ) var/list/hearers = playsound(idle_platform, jingle, 50, FALSE, 0, extrarange= 10, ignore_walls = TRUE) new /datum/threed_sound( - idle_platform, - jingle, - hearers, - FALSE, - 60, - SOUND_RANGE + 7, - 12 SECONDS, - our_channel, - null, - null, - SOUND_FALLOFF_EXPONENT, - 5 + new_parent = idle_platform, + new_sound = jingle, + current_listeners = hearers, + can_add_new_listeners = FALSE, + volume = 60, + sound_range = SOUND_RANGE + 7, + sound_length = 12 SECONDS, + channel = our_channel, + preference_volume = null, + preference_signal = null, + falloff_exponent = SOUND_FALLOFF_EXPONENT, + falloff_distance = 5 ) + tram_registration.distance_travelled += (travel_trip_length - travel_remaining) travel_trip_length = 0 current_speed = 0 diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm index 5b1794ef33bf..dae3132b706d 100644 --- a/code/modules/unit_tests/simple_animal_freeze.dm +++ b/code/modules/unit_tests/simple_animal_freeze.dm @@ -5,9 +5,6 @@ // Use the new basic mobs system instead. // If you are refactoring a simple_animal, REMOVE it from this list var/list/allowed_types = list( - /mob/living/simple_animal/bot, - /mob/living/simple_animal/bot/mulebot, - /mob/living/simple_animal/bot/mulebot/paranormal, /mob/living/simple_animal/hostile, /mob/living/simple_animal/hostile/asteroid, /mob/living/simple_animal/hostile/asteroid/elite, diff --git a/code/modules/wiremod/shell/moneybot.dm b/code/modules/wiremod/shell/moneybot.dm index 2a6aa015faef..73abc89e2dc3 100644 --- a/code/modules/wiremod/shell/moneybot.dm +++ b/code/modules/wiremod/shell/moneybot.dm @@ -16,7 +16,8 @@ var/locked = FALSE /obj/structure/money_bot/atom_deconstruct(disassembled = TRUE) - new /obj/item/holochip(drop_location(), stored_money) + if(stored_money) + new /obj/item/holochip(drop_location(), stored_money) /obj/structure/money_bot/proc/add_money(to_add) stored_money += to_add diff --git a/config/config.txt b/config/config.txt index 5dd48f757487..9c3da111bf80 100644 --- a/config/config.txt +++ b/config/config.txt @@ -25,7 +25,7 @@ $include map_vote.txt # SERVERTAGLINE A generic TG-based server ## Discord Link: This is the link to discord server -# DISCORD_LINK +# DISCORD_LINK ## Server SQL name: This is the name used to identify the server to the SQL DB, distinct from SERVERNAME as it must be at most 32 characters. # SERVERSQLNAME tgstation diff --git a/config/robot_voices.json b/config/robot_voices.json new file mode 100644 index 000000000000..e8bf62bf73cc --- /dev/null +++ b/config/robot_voices.json @@ -0,0 +1,46 @@ +{ + "/datum/customer_data/american": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/italian": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/french": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/japanese": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/japanese/salaryman": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/moth": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/mexican": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/british": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/british/gent": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/british/bobby": [ + "Example Man", + "Example Woman" + ], + "/datum/customer_data/malfunction": [ + "Example Man", + "Example Woman" + ] +} diff --git a/dependencies.sh b/dependencies.sh index bf5a66f6e7c7..d6ef38ae6157 100644 --- a/dependencies.sh +++ b/dependencies.sh @@ -26,7 +26,7 @@ export PYTHON_VERSION=3.11.0 export DREAMLUAU_REPO="tgstation/dreamluau" #dreamluau git tag -export DREAMLUAU_VERSION=0.2.1 +export DREAMLUAU_VERSION=0.2.2 #hypnagogic repo export CUTTER_REPO=spacestation13/hypnagogic diff --git a/dreamluau.dll b/dreamluau.dll index 6975fad39969..350016daae9e 100644 Binary files a/dreamluau.dll and b/dreamluau.dll differ diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index 24ae7d3e2172..2c3e163139a9 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -262,11 +262,16 @@ timothymtorres: - sound: Books now make paper sound when opened 2026-06-11: + AtomTheProphet: + - rscadd: Adds "Conserved Genetics", a plant trait which prevents the plant from + having its instability be altered by cross-pollination. Melbert: - bugfix: Losing your limbs via the cursed quirk now gives you limb stumps (so you can get your limbs back afterwards) SmArtKar: - bugfix: Fixed debug overlays on new biome rocks on lavaland + - bugfix: Fixed diamond and bluespace artifact boulders not containing any diamonds/bluespace + crystals TealSeer: - refactor: security camera construction/item interaction has been rewritten, report any weirdness @@ -286,3 +291,141 @@ - sound: Add valve sound to portable canisters - sound: Add keypad sounds to secure safes - sound: Add drill sound to surgical drill +2026-06-12: + Absolucy, Flleeppyy: + - rscadd: Added a websocket client, you can now forward incoming chat messages to + an external program, i.e allowing you to change music playlists based on events, + or such. + Ghommie: + - admin: Prayers from players are now boxed (like examine messages and some health + readouts for example), making them easier to tell apart from the constant stream + of text in your chat tabs. + - rscadd: Santa Claus can now hear prayers that mention him or Christmas. He won't + know who's praying unless told, though he can tell if they're naughty. + Hatterhat: + - spellcheck: Butterbean plants are now capitalized properly when planted. + Profakos (originally pulled from their branch, did massive chunk of this), Ben10omintrix: + - refactor: mulebots have been refactored. please report any bugs + SmArtKar: + - bugfix: Fixed broken area overlays for particle weather + TymSha: + - qol: Ooze mobs from cytology now have the same type of HUD as other mobs, allowing + them to see their health + - qol: The abilities of shoolean grapes now have their cost specified in their description + - bugfix: The mending globule ability used by shoolean grapes no longer can be cast + while there isn't enough nutrition to do so + antropod: + - bugfix: fixed material tiles and walls not having material when constructed from + last item in the stack + timothymtorres: + - sound: Add on/off sound to light switches in rooms +2026-06-13: + Absolucy: + - qol: You can now preview what different emotes sound like via the emote panel. + Arturlang: + - rscadd: beams now animate when moving tile to tile, this is mostly noticeable + for constant beam effects + - bugfix: beam emissives now work + CabinetOnFire: + - bugfix: You can now hear the outside world when you're inside of something again + (e.g. lockers, mechs + SmArtKar: + - image: Changed tendril regeneration outlinefrom bright green to maroon red + SyncIt21: + - rscdel: removes the welder from the engiborg omnitool. They are now separate entities + again + - qol: The omnitool radial menu won't show the same selected tool again reducing + clutter + timothymtorres: + - code_imp: Reorganize shuttle maps into separate folders +2026-06-14: + CabinetOnFire: + - bugfix: Fixes bug where sig_remove_from_blackboard would try to numerically index + a regular list + FalloutFalcon: + - bugfix: Clients who sit in the lobby after pregame will no longer have there ambience + disabled till they reconnect + Ghommie: + - bugfix: You no longer waddle when buckled to something. + Melbert: + - rscadd: You can now deconstruct first aid stations with a screwdriver and a crowbar. + - rscadd: You can now buy first aid station circuitboards from cargo for 1200 credits. + - rscadd: Most medical supplies ordered from cargo now come in DeForest crates. + Sansufentanyl comes in an Interdyne create. + - balance: As they can now be replaced, first aid station armor has been reduced. + - balance: Emergency first aid stations now heal twice as fast but also take twice + as long to recharge. + SmArtKar: + - bugfix: Fixed rugs runtiming when cleaning objects + - bugfix: Fixed harddels in escape menu + carshalash: + - qol: Made certain cocktails reflect real-world ratios. Including gin fizz, rum + and coke, whiskey cola/soda, screwdriver, and both tonic cocktails. + lelandkemble: + - bugfix: fixed a runtime in pathfinding + - bugfix: fixed a runtime when breaking a moneybot without money + - bugfix: '"fixed" a runtime with gas mask filters' + remove32: + - bugfix: statpanel uses year offset +2026-06-15: + Ben10Omintrix: + - bugfix: baby stoats can now become adults + - rscadd: baby stoats are added to the spawn pool of maints, (the overall chance + of running into a stoat whether adult/baby remains the same) + CabinetOnFire: + - bugfix: Fixes a hard-del and signal override with sound tokens + Iamgoofball: + - rscadd: Added support for Radio TTS. You will now hear players over the radio + via the TTS system. + - rscadd: Configure this in Game Options under the Sound tab. + - sound: Re-did blips to be significantly nicer sounding and way better. + - sound: Alien speech you don't understand now comes through as Blips. If you have + TTS entirely disabled, you won't hear blips. + - sound: The Tram and Computers now utilize the TTS system. Configs have been added + to set a consistent voice. + - sound: TTS audio now utilizes 3D audio; you can now walk away from people saying + stupid shit and it gets quieter. + - sound: Blips also now include more customization options, and a dedicated Blips + preview button by clicking the leaf icon. + - rscdel: The configuration to disable TTS on whispering has been removed, as it + is no longer needed and also interferes with radio TTS functioning properly. + LemonInTheDark: + - bugfix: End of round mapvotes will now properly default to the only option if + only one map is votable. + Melbert: + - qol: Gizmo sends a message when turning on and off + - qol: 'If your gun is empty, and you are on combat mode, and you are adjacent to + someone, and you click on them: you will perform a gun bash rather than just + dry firing.' + SmArtKar: + - bugfix: Fixed weather on multi-z maps + - qol: Particle weather can now be disabled in favor of old weather. !!! AMD GPU + USERS NEED TO SWITCH TO OLD WEATHER TO FIX THE LAG!!! + - bugfix: Fixed recovered crew showing up as uncategorized antags + lelandkemble: + - bugfix: fixed a runtime when fishing up no fish +2026-06-16: + Aliceee2ch: + - balance: Gender changing potion has a 5 seconds do_after now. + CabinetOnFire: + - code_imp: Adds playsoundtoken() which lets you play one shot sound tokens + - sound: You can now hear screams get louder as you get closer, spooky! +2026-06-17: + Melbert: + - qol: Adds an antag info panel for revolution + - qol: Codewords can now consist of any station area, and excludes areas not on + the current station + - qol: Head Revolutionaries (not their underlings) get codewords + SmArtKar: + - bugfix: Fixed slime feeding runtimes + - bugfix: Fixed wraith cloaking module runtimes + - qol: Materials can now be directly ejected from autolathes rather than needing + to be printed. + - bugfix: Fixed a bunch of runtimes caused when you try to perform surgery on cyborgs + (which cannot be done in the first place) + - bugfix: Fixed earthcracker not getting spent on mining z levels + SyncIt21: + - bugfix: Tooltips no longer cover the whole borgs hands held items + levels0: + - rscadd: new gizmode + - bugfix: you can now interact with item gizmo wires diff --git a/icons/hud/actions.dmi b/icons/hud/actions.dmi index e8385331596b..d95481b1ef31 100644 Binary files a/icons/hud/actions.dmi and b/icons/hud/actions.dmi differ diff --git a/icons/obj/machines/wall_healer.dmi b/icons/obj/machines/wall_healer.dmi index f09ad6f7339d..2e53dee7fa57 100644 Binary files a/icons/obj/machines/wall_healer.dmi and b/icons/obj/machines/wall_healer.dmi differ diff --git a/modular_meta/features/ntts-nd-tg-tts/code/middleware.dm b/modular_meta/features/ntts-nd-tg-tts/code/middleware.dm index 3b1ec4c51010..2626f8f4b6a5 100644 --- a/modular_meta/features/ntts-nd-tg-tts/code/middleware.dm +++ b/modular_meta/features/ntts-nd-tg-tts/code/middleware.dm @@ -24,7 +24,7 @@ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, message, speaker = speaker, pitch = pitch, local = TRUE, blip_base = blip_base, blip_number = blip_number) return TRUE -/datum/preference_middleware/tts/play_voice_borg(list/params, mob/user) +/datum/preference_middleware/tts/play_voice_robot(list/params, mob/user) if(!COOLDOWN_FINISHED(src, tts_test_cooldown)) return TRUE var/speaker = params["voice"] diff --git a/modular_meta/features/ntts-nd-tg-tts/code/picker.dm b/modular_meta/features/ntts-nd-tg-tts/code/picker.dm index 33e300c62688..d41404eaf8d9 100644 --- a/modular_meta/features/ntts-nd-tg-tts/code/picker.dm +++ b/modular_meta/features/ntts-nd-tg-tts/code/picker.dm @@ -54,7 +54,7 @@ if("play_voice") return middleware.play_voice(params, ui.user) if("play_robot") - return middleware.play_voice_borg(params, ui.user) + return middleware.play_voice_robot(params, ui.user) if("play_blips") return middleware.play_blips(params, ui.user) if("play_radio") diff --git a/modular_meta/features/security_extended/code/machinery/turnstile.dm b/modular_meta/features/security_extended/code/machinery/turnstile.dm index 797918bd147a..020b795411d5 100644 --- a/modular_meta/features/security_extended/code/machinery/turnstile.dm +++ b/modular_meta/features/security_extended/code/machinery/turnstile.dm @@ -33,7 +33,7 @@ . = ..() if(istype(mover) && (mover.pass_flags & PASSGLASS)) return TRUE - if(istype(mover, /mob/living/simple_animal/bot)) + if(istype(mover, /mob/living/basic/bot)) flick("operate", src) playsound(src,'sound/items/tools/ratchet.ogg',50,0,3) return TRUE diff --git a/strings/locations.json b/strings/locations.json deleted file mode 100644 index 1ae1e628ab51..000000000000 --- a/strings/locations.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "locations": [ - "Aft Maintenance", - "Aft Primary Hallway", - "AI Chamber", - "AI Satellite Antechamber", - "AI Satellite Exterior", - "AI Upload Chamber", - "Armory", - "Atmospherics Engine", - "Atmospherics", - "Atrium", - "Auxiliary Base Construction", - "Auxiliary Restrooms", - "Auxiliary Tool Storage", - "Bar", - "Bridge", - "Brig Control", - "Brig", - "Captain's Office", - "Captain's Quarters", - "Cargo Bay", - "Cargo Office", - "Chapel Office", - "Chapel", - "Chemistry", - "Command Hallway", - "Construction Area", - "Corporate Showroom", - "Council Chamber", - "Courtroom", - "Custodial Closet", - "Customs", - "Cytology Lab", - "Delivery Office", - "Departure Lounge", - "Detective's Office", - "Dormitories", - "Engineering Foyer", - "Engineering Storage", - "Engineering", - "EVA Storage", - "Experimentation Lab", - "Firing Range", - "Gateway", - "Genetics Lab", - "Gravity Generator Room", - "Hydroponics", - "Incinerator", - "Kitchen", - "Law Office", - "Library", - "Locker Room", - "Mech Bay", - "Medbay Central", - "Medbay Maintenance", - "Medbay Storage", - "Mining Office", - "Morgue Maintenance", - "Morgue", - "Primary Tool Storage", - "Prison Wing", - "Prisoner Education Chamber", - "Recreation Area", - "Recreational Holodeck", - "Research and Development", - "Research Division", - "Research Testing Range", - "Restrooms", - "Robotics Lab", - "Science Maintenance", - "Security Checkpoint", - "Security Office", - "Service Hallway", - "Space", - "Supermatter Engine", - "Surgery", - "Technical Storage", - "Teleporter Room", - "Testing Lab", - "Theatre", - "Toxins Mixing Chamber", - "Toxins Mixing Lab", - "Toxins Storage", - "Toxins Test Area", - "Transfer Centre", - "Transit Tube", - "Vacant Commissary", - "Vacant Office", - "Vault", - "Virology", - "Warehouse", - "Waste Disposal", - "Xenobiology Lab" - ] -} diff --git a/strings/modular_maps/emergency_scrapheap.toml b/strings/modular_maps/emergency_scrapheap.toml index 6372704b7040..e6c56d82ffce 100644 --- a/strings/modular_maps/emergency_scrapheap.toml +++ b/strings/modular_maps/emergency_scrapheap.toml @@ -1,4 +1,4 @@ -directory = "_maps/shuttles/emergency_scrapheap/" +directory = "_maps/shuttles/emergency/emergency_scrapheap/" [rooms.shuttle] modules = ["classic1.dmm", "classic2.dmm"] diff --git a/strings/sillytips.txt b/strings/sillytips.txt index fdf740f07a61..71616ba148c4 100644 --- a/strings/sillytips.txt +++ b/strings/sillytips.txt @@ -40,3 +40,4 @@ When a round ends nearly everything about it is lost forever, leave your salt be You can light a match with the heel of your boot. ...What's that? Friction matches went out of style in the 19th century? That's nonsense! You can win a pulse rifle from the arcade machine. Honest. Your sprite represents your hitbox, so that afro makes you easier to kill. The sacrifices we make for style. +Santa Claus will listen to your prayers if you call him. diff --git a/tgstation.dme b/tgstation.dme index 3963035c2edc..c232b7192ff7 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1264,6 +1264,7 @@ #include "code\datums\components\limb_applicable.dm" #include "code\datums\components\ling_decoy_brain.dm" #include "code\datums\components\listen_and_repeat.dm" +#include "code\datums\components\listen_prayers.dm" #include "code\datums\components\lock_on_cursor.dm" #include "code\datums\components\lockable_storage.dm" #include "code\datums\components\magnet.dm" @@ -1839,14 +1840,14 @@ #include "code\datums\martial\sleeping_carp.dm" #include "code\datums\martial\spiders_bite.dm" #include "code\datums\martial\wrestling.dm" -#include "code\datums\material\material_container.dm" -#include "code\datums\material\remote_materials.dm" #include "code\datums\materials\_material.dm" #include "code\datums\materials\alloys.dm" #include "code\datums\materials\basemats.dm" #include "code\datums\materials\hauntium.dm" #include "code\datums\materials\meat.dm" #include "code\datums\materials\pizza.dm" +#include "code\datums\materials\material_container\material_container.dm" +#include "code\datums\materials\material_container\remote_materials.dm" #include "code\datums\materials\material_slots\_slot.dm" #include "code\datums\materials\material_slots\generic.dm" #include "code\datums\materials\properties\_properties.dm" @@ -2042,6 +2043,7 @@ #include "code\datums\shuttles\ferry.dm" #include "code\datums\shuttles\hunter.dm" #include "code\datums\shuttles\infiltrator.dm" +#include "code\datums\shuttles\labour.dm" #include "code\datums\shuttles\mining.dm" #include "code\datums\shuttles\pirate.dm" #include "code\datums\shuttles\ruin.dm" @@ -2743,37 +2745,6 @@ #include "code\game\objects\items\devices\scanners\sequence_scanner.dm" #include "code\game\objects\items\devices\scanners\slime_scanner.dm" #include "code\game\objects\items\devices\scanners\t_scanner.dm" -#include "code\game\objects\items\food\_food.dm" -#include "code\game\objects\items\food\bait.dm" -#include "code\game\objects\items\food\bread.dm" -#include "code\game\objects\items\food\burgers.dm" -#include "code\game\objects\items\food\cake.dm" -#include "code\game\objects\items\food\cheese.dm" -#include "code\game\objects\items\food\donkpocket.dm" -#include "code\game\objects\items\food\donuts.dm" -#include "code\game\objects\items\food\dough.dm" -#include "code\game\objects\items\food\egg.dm" -#include "code\game\objects\items\food\frozen.dm" -#include "code\game\objects\items\food\lizard.dm" -#include "code\game\objects\items\food\martian.dm" -#include "code\game\objects\items\food\meatdish.dm" -#include "code\game\objects\items\food\meatslab.dm" -#include "code\game\objects\items\food\mexican.dm" -#include "code\game\objects\items\food\misc.dm" -#include "code\game\objects\items\food\monkeycube.dm" -#include "code\game\objects\items\food\moth.dm" -#include "code\game\objects\items\food\packaged.dm" -#include "code\game\objects\items\food\pancakes.dm" -#include "code\game\objects\items\food\pastries.dm" -#include "code\game\objects\items\food\pie.dm" -#include "code\game\objects\items\food\pizza.dm" -#include "code\game\objects\items\food\salad.dm" -#include "code\game\objects\items\food\sandwichtoast.dm" -#include "code\game\objects\items\food\snacks.dm" -#include "code\game\objects\items\food\soup.dm" -#include "code\game\objects\items\food\spaghetti.dm" -#include "code\game\objects\items\food\sweets.dm" -#include "code\game\objects\items\food\vegetables.dm" #include "code\game\objects\items\granters\_granters.dm" #include "code\game\objects\items\granters\chuuni_granter.dm" #include "code\game\objects\items\granters\oragami.dm" @@ -4170,6 +4141,7 @@ #include "code\modules\client\preferences\paint_color.dm" #include "code\modules\client\preferences\parallax.dm" #include "code\modules\client\preferences\paraplegic.dm" +#include "code\modules\client\preferences\particle_weather.dm" #include "code\modules\client\preferences\pda.dm" #include "code\modules\client\preferences\persistent_scars.dm" #include "code\modules\client\preferences\phobia.dm" @@ -4599,6 +4571,37 @@ #include "code\modules\food_and_drinks\pizzabox.dm" #include "code\modules\food_and_drinks\equipment\kitchen_utensils.dm" #include "code\modules\food_and_drinks\equipment\plate.dm" +#include "code\modules\food_and_drinks\food\_food.dm" +#include "code\modules\food_and_drinks\food\bait.dm" +#include "code\modules\food_and_drinks\food\bread.dm" +#include "code\modules\food_and_drinks\food\burgers.dm" +#include "code\modules\food_and_drinks\food\cake.dm" +#include "code\modules\food_and_drinks\food\cheese.dm" +#include "code\modules\food_and_drinks\food\donkpocket.dm" +#include "code\modules\food_and_drinks\food\donuts.dm" +#include "code\modules\food_and_drinks\food\dough.dm" +#include "code\modules\food_and_drinks\food\egg.dm" +#include "code\modules\food_and_drinks\food\frozen.dm" +#include "code\modules\food_and_drinks\food\lizard.dm" +#include "code\modules\food_and_drinks\food\martian.dm" +#include "code\modules\food_and_drinks\food\meatdish.dm" +#include "code\modules\food_and_drinks\food\meatslab.dm" +#include "code\modules\food_and_drinks\food\mexican.dm" +#include "code\modules\food_and_drinks\food\misc.dm" +#include "code\modules\food_and_drinks\food\monkeycube.dm" +#include "code\modules\food_and_drinks\food\moth.dm" +#include "code\modules\food_and_drinks\food\packaged.dm" +#include "code\modules\food_and_drinks\food\pancakes.dm" +#include "code\modules\food_and_drinks\food\pastries.dm" +#include "code\modules\food_and_drinks\food\pie.dm" +#include "code\modules\food_and_drinks\food\pizza.dm" +#include "code\modules\food_and_drinks\food\salad.dm" +#include "code\modules\food_and_drinks\food\sandwichtoast.dm" +#include "code\modules\food_and_drinks\food\snacks.dm" +#include "code\modules\food_and_drinks\food\soup.dm" +#include "code\modules\food_and_drinks\food\spaghetti.dm" +#include "code\modules\food_and_drinks\food\sweets.dm" +#include "code\modules\food_and_drinks\food\vegetables.dm" #include "code\modules\food_and_drinks\machinery\coffeemaker.dm" #include "code\modules\food_and_drinks\machinery\deep_fryer.dm" #include "code\modules\food_and_drinks\machinery\food_cart.dm" @@ -5292,6 +5295,14 @@ #include "code\modules\mob\living\basic\bots\hygienebot\hygienebot_ai.dm" #include "code\modules\mob\living\basic\bots\medbot\medbot.dm" #include "code\modules\mob\living\basic\bots\medbot\medbot_ai.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot_ai.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot_control.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot_delivery.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot_hud.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot_movement.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot_paranormal.dm" +#include "code\modules\mob\living\basic\bots\mulebot\mulebot_tool_interactions.dm" #include "code\modules\mob\living\basic\bots\repairbot\repairbot.dm" #include "code\modules\mob\living\basic\bots\repairbot\repairbot_abilities.dm" #include "code\modules\mob\living\basic\bots\repairbot\repairbot_ai.dm" @@ -5778,10 +5789,8 @@ #include "code\modules\mob\living\simple_animal\animal_defense.dm" #include "code\modules\mob\living\simple_animal\damage_procs.dm" #include "code\modules\mob\living\simple_animal\simple_animal.dm" -#include "code\modules\mob\living\simple_animal\bot\bot.dm" #include "code\modules\mob\living\simple_animal\bot\bot_announcement.dm" #include "code\modules\mob\living\simple_animal\bot\construction.dm" -#include "code\modules\mob\living\simple_animal\bot\mulebot.dm" #include "code\modules\mob\living\simple_animal\hostile\hostile.dm" #include "code\modules\mob\living\simple_animal\hostile\ooze.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\_megafauna.dm" @@ -6379,6 +6388,7 @@ #include "code\modules\research\gizmo\gizmodes\giz_filler.dm" #include "code\modules\research\gizmo\gizmodes\gizactives.dm" #include "code\modules\research\gizmo\gizmodes\gizbad.dm" +#include "code\modules\research\gizmo\gizmodes\gizcode.dm" #include "code\modules\research\gizmo\gizmodes\gizcopier.dm" #include "code\modules\research\gizmo\gizmodes\gizlectric.dm" #include "code\modules\research\gizmo\gizmodes\gizmisc.dm" diff --git a/tgui/packages/tgui-panel/index.tsx b/tgui/packages/tgui-panel/index.tsx index 8ccc51fd16e1..c4d23c2d5eb2 100644 --- a/tgui/packages/tgui-panel/index.tsx +++ b/tgui/packages/tgui-panel/index.tsx @@ -14,6 +14,7 @@ import { setupHotReloading } from 'tgui-dev-server/link/client'; import { App } from './app'; import { bus } from './events/listeners'; import { setupPanelFocusHacks } from './panelFocus'; +import { wsSend } from './websocket/helpers'; const root = createRoot(document.getElementById('react-root')!); @@ -38,7 +39,10 @@ function setupApp() { render(); // Dispatch incoming messages as store actions - Byond.subscribe((type, payload) => bus.dispatch({ type, payload })); + Byond.subscribe((type, payload) => { + bus.dispatch({ type, payload }); + wsSend({ type, payload }); + }); // Unhide the panel Byond.winset('output_selector.legacy_output_selector', { diff --git a/tgui/packages/tgui-panel/settings/SettingsPanel.tsx b/tgui/packages/tgui-panel/settings/SettingsPanel.tsx index 1efcfe8facfa..7e8b62ab596a 100644 --- a/tgui/packages/tgui-panel/settings/SettingsPanel.tsx +++ b/tgui/packages/tgui-panel/settings/SettingsPanel.tsx @@ -9,6 +9,7 @@ import { ChatPageSettings } from '../chat/ChatPageSettings'; import { SETTINGS_TABS } from './constants'; import { SettingsGeneral } from './SettingsGeneral'; import { SettingsStatPanel } from './SettingsStatPanel'; +import { SettingsWebsocket } from './SettingsWebsocket'; import { TextHighlightSettings } from './TextHighlight'; import { useSettings } from './use-settings'; @@ -48,6 +49,7 @@ export function SettingsPanel(props) { {activeTab === 'chatPage' && } {activeTab === 'textHighlight' && } {activeTab === 'statPanel' && } + {activeTab === 'websocket' && } ); diff --git a/tgui/packages/tgui-panel/settings/SettingsWebsocket.tsx b/tgui/packages/tgui-panel/settings/SettingsWebsocket.tsx new file mode 100644 index 000000000000..4905b4b3f1d7 --- /dev/null +++ b/tgui/packages/tgui-panel/settings/SettingsWebsocket.tsx @@ -0,0 +1,94 @@ +import { + Button, + Input, + LabeledList, + Section, + Stack, +} from 'tgui-core/components'; +import { chatRenderer } from 'tgui-panel/chat/renderer'; +import { + wsDisconnect, + wsReconnect, + wsUpdate, +} from 'tgui-panel/websocket/helpers'; +import { useSettings } from './use-settings'; + +export function SettingsWebsocket(props) { + const { settings, updateSettings } = useSettings(); + const { statLinked, statFontSize, statTabsStyle } = settings; + + return ( +
+ + + + + { + const websocketEnabled = !settings.websocketEnabled; + updateSettings({ websocketEnabled }); + wsUpdate(websocketEnabled); + }} + > + Enabled + + + + + + + +
+ ); +} diff --git a/tgui/packages/tgui-panel/settings/atoms.ts b/tgui/packages/tgui-panel/settings/atoms.ts index 275d5b8f2fb3..dbf2bb652f08 100644 --- a/tgui/packages/tgui-panel/settings/atoms.ts +++ b/tgui/packages/tgui-panel/settings/atoms.ts @@ -17,6 +17,8 @@ export const defaultSettings: SettingsState = { visible: false, activeTab: SETTINGS_TABS[0].id, }, + websocketEnabled: false, + websocketServer: '', }; export const defaultHighlightSetting: HighlightSetting = { diff --git a/tgui/packages/tgui-panel/settings/constants.ts b/tgui/packages/tgui-panel/settings/constants.ts index 2e5056f785e7..3b7f4592b044 100644 --- a/tgui/packages/tgui-panel/settings/constants.ts +++ b/tgui/packages/tgui-panel/settings/constants.ts @@ -43,6 +43,10 @@ export const SETTINGS_TABS = [ id: 'statPanel', name: 'Stat Panel', }, + { + id: 'websocket', + name: 'Websocket', + }, ] as const; export const FONTS_DISABLED = 'Default'; diff --git a/tgui/packages/tgui-panel/settings/migration.ts b/tgui/packages/tgui-panel/settings/migration.ts index d90f89fcd44d..01318e0cb241 100644 --- a/tgui/packages/tgui-panel/settings/migration.ts +++ b/tgui/packages/tgui-panel/settings/migration.ts @@ -1,6 +1,7 @@ import { storage } from 'common/storage'; import { smoothMerge } from 'common/type-safety'; import { omit, pick } from 'es-toolkit'; +import { wsUpdate } from 'tgui-panel/websocket/helpers'; import { setMusicVolume } from '../audio/handlers'; import { chatRenderer } from '../chat/renderer'; import { store } from '../events/store'; @@ -111,6 +112,11 @@ export function startSettingsMigration(next: MergedSettings): void { store.set(settingsAtom, draftSettings); console.log('Migrated panel settings:', draftSettings); + if (draftSettings.websocketEnabled !== defaultSettings.websocketEnabled) { + // Ensure websocket state is correct after migration + wsUpdate(draftSettings.websocketEnabled); + } + const migratedHighlights = migrateHighlights(highlightPart); // Just exit if no valid version was found diff --git a/tgui/packages/tgui-panel/settings/types.ts b/tgui/packages/tgui-panel/settings/types.ts index f7284e41e241..d77dc93fa5ea 100644 --- a/tgui/packages/tgui-panel/settings/types.ts +++ b/tgui/packages/tgui-panel/settings/types.ts @@ -18,6 +18,8 @@ export const settingsSchema = z.object({ theme: z.string(), version: z.number(), view: viewSchema, + websocketEnabled: z.boolean(), + websocketServer: z.string(), }); export type HighlightSetting = { diff --git a/tgui/packages/tgui-panel/websocket/helpers.ts b/tgui/packages/tgui-panel/websocket/helpers.ts new file mode 100644 index 000000000000..cc1db416dff7 --- /dev/null +++ b/tgui/packages/tgui-panel/websocket/helpers.ts @@ -0,0 +1,190 @@ +import { createLogger } from 'tgui/logging'; +import { chatRenderer } from 'tgui-panel/chat/renderer'; +import { store } from 'tgui-panel/events/store'; +import { settingsAtom } from 'tgui-panel/settings/atoms'; + +const MAX_RETRIES = 10; +const RETRY_INTERVAL = 500; // ms + +// Websocket close codes +const WEBSOCKET_DISABLED = 4555; +const WEBSOCKET_REATTEMPT = 4556; +const SAFE_CLOSE_CODE = 1000; + +const logger = createLogger('websocket'); + +let websocket: WebSocket | null = null; +let reconnectTimer: number | null = null; +let retryCount = 0; +let manuallyClosed = false; + +function sendWSNotice(message, small = false) { + chatRenderer.processBatch([ + { + html: small + ? `${message}` + : `
${message}
`, + }, + ]); +} + +function clearReconnectTimer() { + if (reconnectTimer !== null) { + clearInterval(reconnectTimer); + reconnectTimer = null; + } +} + +function safeClose(code = SAFE_CLOSE_CODE, reason?: string) { + if (!websocket) return; + if ( + websocket.readyState === WebSocket.CLOSED || + websocket.readyState === WebSocket.CLOSING + ) { + return; + } + + websocket.close(code, reason); +} + +function startReconnectLoop() { + if (reconnectTimer !== null) return; + + reconnectTimer = window.setInterval(() => { + const { websocketEnabled } = store.get(settingsAtom); + + if (!websocketEnabled) { + clearReconnectTimer(); + return; + } + + if (retryCount >= MAX_RETRIES) { + clearReconnectTimer(); + sendWSNotice( + `Websocket failed to reconnect after ${MAX_RETRIES} attempts.`, + true, + ); + return; + } + + if ( + !websocket || + websocket.readyState === WebSocket.CLOSED || + websocket.readyState === WebSocket.CLOSING + ) { + retryCount++; + setupWebsocket(); + } + }, RETRY_INTERVAL); +} + +function setupWebsocket(force = false) { + const { websocketEnabled, websocketServer } = store.get(settingsAtom); + + if (!websocketEnabled) { + clearReconnectTimer(); + safeClose(WEBSOCKET_DISABLED); + websocket = null; + return; + } + + if (!force && websocket && websocket.readyState === WebSocket.OPEN) { + return; + } + + if (force) { + clearReconnectTimer(); + + if (websocket) { + manuallyClosed = true; + try { + websocket.close(WEBSOCKET_REATTEMPT, 'forced reconnect'); + } catch { + /* ignore */ + } + websocket = null; + } + } + + try { + manuallyClosed = false; + websocket = new WebSocket(`ws://${websocketServer}`); + } catch (e: any) { + if (e.name === 'SyntaxError') { + sendWSNotice( + `Error creating websocket: Invalid address! Make sure you're following the placeholder. Example: localhost:1234`, + true, + ); + return; + } + sendWSNotice(`Error creating websocket: ${e.name} - ${e.message}`); + startReconnectLoop(); + return; + } + + websocket.addEventListener('open', () => { + clearReconnectTimer(); + sendWSNotice('Websocket connected!', true); + Byond.sendMessage('requestMetadata'); // let's be nice and request metadata + retryCount = 0; + }); + + websocket.addEventListener('message', (event) => { + if (event.data === 'requestMetadata') { + Byond.sendMessage('requestMetadata'); + } + }); + + websocket.addEventListener('close', (ev) => { + websocket = null; + + if (manuallyClosed || ev.code === WEBSOCKET_DISABLED) return; + + sendWSNotice( + `Websocket disconnected! Code: ${ev.code} Reason: ${ev.reason || 'None provided'}`, + true, + ); + + startReconnectLoop(); + }); + + websocket.addEventListener('error', (ev) => { + logger.error('got websocket error', ev); + safeClose(WEBSOCKET_REATTEMPT, 'got error from server'); + }); +} + +// Initial connect +setupWebsocket(); + +export function wsUpdate(enabled: boolean): void { + if (enabled) { + setupWebsocket(); + return; + } + + manuallyClosed = true; + clearReconnectTimer(); + safeClose(WEBSOCKET_DISABLED); + websocket = null; +} + +export function wsReconnect(): void { + setupWebsocket(true); + sendWSNotice('Attempting to connect to websocket...', true); +} + +export function wsDisconnect(): void { + manuallyClosed = true; + clearReconnectTimer(); + safeClose(WEBSOCKET_DISABLED); + websocket = null; + retryCount = 0; + sendWSNotice('Websocket forcefully disconnected. (Retry count reset)', true); +} + +export function wsSend(msg: Record): void { + if (websocket?.readyState === WebSocket.OPEN) { + websocket.send(JSON.stringify(msg)); + } +} diff --git a/tgui/packages/tgui/interfaces/AntagInfoRevolution.tsx b/tgui/packages/tgui/interfaces/AntagInfoRevolution.tsx new file mode 100644 index 000000000000..ccd0addf0902 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AntagInfoRevolution.tsx @@ -0,0 +1,128 @@ +import { Section, Stack } from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; +import { useBackend } from '../backend'; +import { Window } from '../layouts'; +import type { Objective } from './common/Objectives'; + +type Head = { + name: string; + role: string; +}; + +type Info = { + antag_name: string; + objectives: Objective[]; + leader: BooleanLike; + code_phrases?: string[]; + code_responses?: string[]; + heads: Head[]; + lone_wolf: BooleanLike; +}; + +// Takes [a, b, c] and returns "a, b, and c" +function formatCodes(text: string[]) { + if (text.length === 0) return ''; + if (text.length === 1) return text[0]; + if (text.length === 2) return `${text[0]} and ${text[1]}`; + return `${text.slice(0, -1).join(', ')}, and ${text[text.length - 1]}`; +} + +export const AntagInfoRevolution = () => { + const { data } = useBackend(); + const { leader, code_phrases, code_responses, heads, lone_wolf } = data; + return ( + + +
+ + + Viva la Revolution! + + + {leader ? ( + + {lone_wolf ? ( + + - You are a lone leader of the revolution. It is + recommended to act swiftly and decisively - when the + revolution is sufficiently large, more leaders will be + promoted. + + ) : ( + + - There are multiple leaders of the revolution. It is + recommended to work together and establish a plan BEFORE + you start converting the crew - being outed early can + prove extremely detrimental. + + )} + + - Convert the crew to your cause with a flash - any flash + will work. + + + - Mindshields will prevent conversion. You can identify them + via the flashing blue border around their job icon. + + + - The revolution is lost if you and your fellow leaders are + all killed or exiled. Do not let that happen! + + + ) : ( + + + - Help your cause. Do not harm your fellow freedom fighters. + + + - You can identify your comrades by the red "R" icons, and + your leaders by the blue "R" icons. + + + - The revolution is lost if all of your leaders are killed + or exiled. Do not let that happen! + + + )} + + {heads.length > 0 && ( + <> + + + + + You must kill or exile the heads of staff: + + {heads.map((head, i) => ( + + - {head.name}, the {head.role} + + ))} + + + + )} + {code_phrases?.length && code_responses?.length && ( + <> + + + + + To identify your fellow leaders, use the following code: + + + Phrases: {formatCodes(code_phrases)} + + + Responses: {formatCodes(code_responses)} + + + + + )} + +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Autolathe.tsx b/tgui/packages/tgui/interfaces/Autolathe.tsx index a9e2f1720e58..d62f83198bb0 100644 --- a/tgui/packages/tgui/interfaces/Autolathe.tsx +++ b/tgui/packages/tgui/interfaces/Autolathe.tsx @@ -16,8 +16,7 @@ import { useBackend } from '../backend'; import { Window } from '../layouts'; import { DesignBrowser } from './Fabrication/DesignBrowser'; import { MaterialCostSequence } from './Fabrication/MaterialCostSequence'; -import type { Design, MaterialMap } from './Fabrication/Types'; -import type { Material } from './Fabrication/Types'; +import type { Design, Material, MaterialMap } from './Fabrication/Types'; type AutolatheDesign = Design & { customMaterials: BooleanLike; @@ -33,7 +32,7 @@ type AutolatheData = { }; export const Autolathe = (props) => { - const { data } = useBackend(); + const { act, data } = useBackend(); const { materialtotal, materialsmax, @@ -84,20 +83,51 @@ export const Autolathe = (props) => { key={material.name} label={capitalize(material.name)} > - -
- {material.amount / SHEET_MATERIAL_AMOUNT + - ' sheets'} -
-
+ + + +
+ {material.amount / SHEET_MATERIAL_AMOUNT + + ' sheets'} +
+
+
+ Eject: + {[ + 1, + 5, + Math.floor( + material.amount / SHEET_MATERIAL_AMOUNT, + ), + ] + .sort() + .map((amt) => ( + + + + ))} +
))} diff --git a/tgui/packages/tgui/interfaces/EmotePanel.tsx b/tgui/packages/tgui/interfaces/EmotePanel.tsx index 674f0ec48238..227c96e64f2f 100644 --- a/tgui/packages/tgui/interfaces/EmotePanel.tsx +++ b/tgui/packages/tgui/interfaces/EmotePanel.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { Box, Button, Flex, Icon, Section } from 'tgui-core/components'; +import { useMemo, useState } from 'react'; +import { Box, Button, Flex, Icon, Section, Stack } from 'tgui-core/components'; import type { BooleanLike } from 'tgui-core/react'; import { capitalizeFirst } from 'tgui-core/string'; @@ -43,6 +43,34 @@ export const EmotePanelContent = (props) => { const [showIcons, toggleShowIcons] = useState(false); + const filteredEmotes = useMemo( + () => + emotes + .filter( + (emote) => + emote.key && + (searchText.length > 0 + ? emote.key.toLowerCase().includes(searchText.toLowerCase()) || + emote.name.toLowerCase().includes(searchText.toLowerCase()) + : true) && + (filterVisible ? emote.visible : true) && + (filterAudible ? emote.audible : true) && + (filterSound ? emote.sound : true) && + (filterHands ? emote.hands : true) && + (filterUseParams ? emote.use_params : true), + ) + .sort((a, b) => (a.name > b.name ? 1 : -1)), + [ + emotes, + searchText, + filterVisible, + filterAudible, + filterSound, + filterHands, + filterUseParams, + ], + ); + return (
{ > - {emotes - .filter( - (emote) => - emote.key && - (searchText.length > 0 - ? emote.key - .toLowerCase() - .includes(searchText.toLowerCase()) || - emote.name - .toLowerCase() - .includes(searchText.toLowerCase()) - : true) && - (filterVisible ? emote.visible : true) && - (filterAudible ? emote.audible : true) && - (filterSound ? emote.sound : true) && - (filterHands ? emote.hands : true) && - (filterUseParams ? emote.use_params : true), - ) - .sort((a, b) => (a.name > b.name ? 1 : -1)) - .map((emote) => ( - - ))} + {filteredEmotes.map((emote) => ( + + act('play_emote', { + emote_key: emote.key, + use_params: useParams, + }) + } + onPreview={() => + act('preview_sound', { + emote_key: emote.key, + }) + } + /> + ))}
@@ -204,6 +190,76 @@ export const EmotePanelContent = (props) => { ); }; +type EmoteButtonProps = { + emote: Emote; + showIcons: boolean; + showNames: boolean; + onPlay: () => void; + onPreview: () => void; +}; + +const EmoteButton = (props: EmoteButtonProps) => { + const { emote, showIcons, showNames, onPlay, onPreview } = props; + + return ( + + + + + {emote.sound ? ( + + - {/*MASSMETA EDIT END (ntts && tgtts) */} + {/*MASSMETA EDIT END (ntts && tgtts) */} ); diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/particle_weather.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/particle_weather.tsx new file mode 100644 index 000000000000..cf4824e6ddbd --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/particle_weather.tsx @@ -0,0 +1,9 @@ +import { CheckboxInput, type FeatureToggle } from '../base'; + +export const particle_weather: FeatureToggle = { + name: 'Enable fancy (particle) weather (AMD GPU incompatible)', + category: 'GAMEPLAY', + description: + 'Enable fancy particle weather. Incompatible with AMD GPUs and will cause heavy lag on them due to a BYOND bug.', + component: CheckboxInput, +}; diff --git a/tools/UpdatePaths/Scripts/96500_mulebots.txt b/tools/UpdatePaths/Scripts/96500_mulebots.txt new file mode 100644 index 000000000000..1393f74f32a0 --- /dev/null +++ b/tools/UpdatePaths/Scripts/96500_mulebots.txt @@ -0,0 +1,2 @@ +/mob/living/simple_animal/bot/mulebot : /mob/living/basic/bot/mulebot{@OLD} +/mob/living/simple_animal/bot/mulebot/paranormal : /mob/living/basic/bot/mulebot/paranormal{@OLD} \ No newline at end of file diff --git a/tools/maplint/lints/area_noop.yml b/tools/maplint/lints/area_noop.yml index c751e85381ba..fedfffd50408 100644 --- a/tools/maplint/lints/area_noop.yml +++ b/tools/maplint/lints/area_noop.yml @@ -9,7 +9,7 @@ skip_files: - _maps/map_files/tramstation/maintenance_modules - _maps/modular_generic - - _maps/shuttles/emergency_scrapheap + - _maps/shuttles/emergency/emergency_scrapheap - _maps/templates - _maps/minigame/deathmatch/OSHA_Violator.dmm - _maps/minigame/deathmatch/maint_mania.dmm