diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_puzzle.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_puzzle.dmm new file mode 100644 index 0000000000..911ee904fe --- /dev/null +++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_puzzle.dmm @@ -0,0 +1,47 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"a" = ( +/turf/template_noop, +/area/lavaland/surface/outdoors) +"b" = ( +/obj/effect/sliding_puzzle/lavaland, +/turf/open/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"c" = ( +/turf/open/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) + +(1,1,1) = {" +a +a +a +a +a +"} +(2,1,1) = {" +a +c +c +c +a +"} +(3,1,1) = {" +a +c +b +c +a +"} +(4,1,1) = {" +a +c +c +c +a +"} +(5,1,1) = {" +a +a +a +a +a +"} diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index 255464d406..d43addf25a 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -70,6 +70,7 @@ #define ADMIN_PUNISHMENT_FIREBALL "Fireball" #define ADMIN_PUNISHMENT_ROD "Immovable Rod" #define ADMIN_PUNISHMENT_SUPPLYPOD "Supply Pod" +#define ADMIN_PUNISHMENT_MAZING "Puzzle" #define AHELP_ACTIVE 1 #define AHELP_CLOSED 2 diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index f98de83c23..b79e303d26 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -72,6 +72,9 @@ if(check_click_intercept(params,A)) return + if(notransform) + return + var/list/modifiers = params2list(params) if(modifiers["shift"] && modifiers["middle"]) ShiftMiddleClickOn(A) diff --git a/code/controllers/subsystem/npcpool.dm b/code/controllers/subsystem/npcpool.dm index 67e31b6931..d93f1f2407 100644 --- a/code/controllers/subsystem/npcpool.dm +++ b/code/controllers/subsystem/npcpool.dm @@ -23,7 +23,7 @@ SUBSYSTEM_DEF(npcpool) var/mob/living/simple_animal/SA = currentrun[currentrun.len] --currentrun.len - if(!SA.ckey) + if(!SA.ckey && !SA.notransform) if(SA.stat != DEAD) SA.handle_automated_movement() if(SA.stat != DEAD) diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm index af299aaee9..4006c13a2c 100644 --- a/code/datums/ruins/lavaland.dm +++ b/code/datums/ruins/lavaland.dm @@ -215,3 +215,10 @@ suffix = "lavaland_surface_random_ripley.dmm" allow_duplicates = FALSE cost = 5 + +/datum/map_template/ruin/lavaland/puzzle + name = "Ancient Puzzle" + id = "puzzle" + description = "Mystery to be solved." + suffix = "lavaland_surface_puzzle.dmm" + cost = 5 \ No newline at end of file diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 0184c4b34f..de30b69f97 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -1313,7 +1313,7 @@ GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits if(!check_rights(R_ADMIN)) return - var/list/punishment_list = list(ADMIN_PUNISHMENT_LIGHTNING, ADMIN_PUNISHMENT_BRAINDAMAGE, ADMIN_PUNISHMENT_GIB, ADMIN_PUNISHMENT_BSA, ADMIN_PUNISHMENT_FIREBALL, ADMIN_PUNISHMENT_ROD, ADMIN_PUNISHMENT_SUPPLYPOD) + var/list/punishment_list = list(ADMIN_PUNISHMENT_LIGHTNING, ADMIN_PUNISHMENT_BRAINDAMAGE, ADMIN_PUNISHMENT_GIB, ADMIN_PUNISHMENT_BSA, ADMIN_PUNISHMENT_FIREBALL, ADMIN_PUNISHMENT_ROD, ADMIN_PUNISHMENT_SUPPLYPOD, ADMIN_PUNISHMENT_MAZING) var/punishment = input("Choose a punishment", "DIVINE SMITING") as null|anything in punishment_list @@ -1359,6 +1359,10 @@ GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits if(iscarbon(target)) target.Stun(10)//takes 0.53 seconds for CentCom pod to land new /obj/effect/DPtarget(get_turf(target), delivery, POD_CENTCOM) + if(ADMIN_PUNISHMENT_MAZING) + if(!puzzle_imprison(target)) + to_chat(usr,"Imprisonment failed!") + return var/msg = "[key_name_admin(usr)] punished [key_name_admin(target)] with [punishment]." message_admins(msg) diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index 002efdbf8d..5345410005 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -1315,3 +1315,18 @@ for(var/t in RANGE_TURFS(1, T)) var/obj/effect/temp_visual/hierophant/blast/B = new(t, user, friendly_fire_check) B.damage = 15 //keeps monster damage boost due to lower damage + + +//Just some minor stuff +/obj/structure/closet/crate/necropolis/puzzle + name = "puzzling chest" + +/obj/structure/closet/crate/necropolis/puzzle/PopulateContents() + var/loot = rand(1,3) + switch(loot) + if(1) + new /obj/item/soulstone/anybody(src) + if(2) + new /obj/item/wisp_lantern(src) + if(3) + new /obj/item/prisoncube(src) \ No newline at end of file diff --git a/code/modules/ruins/lavalandruin_code/puzzle.dm b/code/modules/ruins/lavalandruin_code/puzzle.dm new file mode 100644 index 0000000000..9e798507da --- /dev/null +++ b/code/modules/ruins/lavalandruin_code/puzzle.dm @@ -0,0 +1,351 @@ +/obj/effect/sliding_puzzle + name = "Sliding puzzle generator" + icon = 'icons/obj/items_and_weapons.dmi' //mapping + icon_state = "syndballoon" + invisibility = INVISIBILITY_ABSTRACT + anchored = TRUE + var/list/elements + var/floor_type = /turf/open/floor/vault + var/finished = FALSE + var/reward_type = /obj/item/reagent_containers/food/snacks/cookie + var/element_type = /obj/structure/puzzle_element + var/auto_setup = TRUE + var/empty_tile_id + +//Gets the turf where the tile with given id should be +/obj/effect/sliding_puzzle/proc/get_turf_for_id(id) + var/turf/center = get_turf(src) + switch(id) + if(1) + return get_step(center,NORTHWEST) + if(2) + return get_step(center,NORTH) + if(3) + return get_step(center,NORTHEAST) + if(4) + return get_step(center,WEST) + if(5) + return center + if(6) + return get_step(center,EAST) + if(7) + return get_step(center,SOUTHWEST) + if(8) + return get_step(center,SOUTH) + if(9) + return get_step(center,SOUTHEAST) + +/obj/effect/sliding_puzzle/Initialize(mapload) + ..() + return INITIALIZE_HINT_LATELOAD + +/obj/effect/sliding_puzzle/LateInitialize() + if(auto_setup) + setup() + +/obj/effect/sliding_puzzle/proc/check_setup_location() + for(var/id in 1 to 9) + var/turf/T = get_turf_for_id(id) + if(!T) + return FALSE + if(istype(T,/turf/closed/indestructible)) + return FALSE + return TRUE + + +/obj/effect/sliding_puzzle/proc/validate() + if(finished) + return + + if(elements.len < 8) //Someone broke it + qdel(src) + + //Check if everything is in place + for(var/id in 1 to 9) + var/target_turf = get_turf_for_id(id) + var/obj/structure/puzzle_element/E = locate() in target_turf + if(id == empty_tile_id && !E) // This location should be empty. + continue + if(!E || E.id != id) //wrong tile or no tile at all + return + //Ding ding + finish() + +/obj/effect/sliding_puzzle/Destroy() + if(LAZYLEN(elements)) + for(var/obj/structure/puzzle_element/E in elements) + E.source = null + elements.Cut() + return ..() + +#define COLLAPSE_DURATION 7 + +/obj/effect/sliding_puzzle/proc/finish() + finished = TRUE + for(var/mob/M in range(7,src)) + shake_camera(M, COLLAPSE_DURATION , 1) + for(var/obj/structure/puzzle_element/E in elements) + E.collapse() + + dispense_reward() + +/obj/effect/sliding_puzzle/proc/dispense_reward() + new reward_type(get_turf(src)) + +/obj/effect/sliding_puzzle/proc/is_solvable() + var/list/current_ordering = list() + for(var/obj/structure/puzzle_element/E in elements_in_order()) + current_ordering += E.id + + var/swap_tally = 0 + for(var/i in 1 to current_ordering.len) + var/checked_value = current_ordering[i] + for(var/j in i to current_ordering.len) + if(current_ordering[j] < checked_value) + swap_tally++ + + return swap_tally % 2 == 0 + +//swap two tiles in same row +/obj/effect/sliding_puzzle/proc/make_solvable() + var/first_tile_id = 1 + var/other_tile_id = 2 + if(empty_tile_id == 1 || empty_tile_id == 2) //Can't swap with empty one so just grab some in second row + first_tile_id = 4 + other_tile_id = 5 + + var/turf/T1 = get_turf_for_id(first_tile_id) + var/turf/T2 = get_turf_for_id(other_tile_id) + + var/obj/structure/puzzle_element/E1 = locate() in T1 + var/obj/structure/puzzle_element/E2 = locate() in T2 + + E1.forceMove(T2) + E2.forceMove(T1) + +/proc/cmp_xy_desc(atom/movable/A,atom/movable/B) + if(A.y > B.y) + return -1 + if(A.y < B.y) + return 1 + if(A.x > B.x) + return 1 + if(A.x < B.x) + return -1 + return 0 + +/obj/effect/sliding_puzzle/proc/elements_in_order() + return sortTim(elements,cmp=/proc/cmp_xy_desc) + +/obj/effect/sliding_puzzle/proc/get_base_icon() + var/icon/I = new('icons/obj/puzzle.dmi') + var/list/puzzles = icon_states(I) + var/puzzle_state = pick(puzzles) + var/icon/P = new('icons/obj/puzzle.dmi',puzzle_state) + return P + +/obj/effect/sliding_puzzle/proc/setup() + //First we slice the 96x96 icon into 32x32 pieces + var/list/puzzle_pieces = list() //id -> icon list + + var/width = 3 + var/height = 3 + var/list/left_ids = list() + var/tile_count = width * height + + //Generate per tile icons + for(var/id in 1 to tile_count) + var/y = width - round((id - 1) / width) + var/x = ((id - 1) % width) + 1 + + var/x_start = 1 + (x - 1) * world.icon_size + var/x_end = x_start + world.icon_size - 1 + var/y_start = 1 + ((y - 1) * world.icon_size) + var/y_end = y_start + world.icon_size - 1 + + var/icon/T = get_base_icon() + T.Crop(x_start,y_start,x_end,y_end) + puzzle_pieces["[id]"] = T + left_ids += id + + //Setup random empty tile + empty_tile_id = pick_n_take(left_ids) + var/turf/empty_tile_turf = get_turf_for_id(empty_tile_id) + empty_tile_turf.PlaceOnTop(floor_type,null,CHANGETURF_INHERIT_AIR) + var/mutable_appearance/MA = new(puzzle_pieces["[empty_tile_id]"]) + MA.layer = empty_tile_turf.layer + 0.1 + empty_tile_turf.add_overlay(MA) + + elements = list() + var/list/empty_spots = left_ids.Copy() + for(var/spot_id in empty_spots) + var/turf/T = get_turf_for_id(spot_id) + T = T.PlaceOnTop(floor_type,null,CHANGETURF_INHERIT_AIR) + var/obj/structure/puzzle_element/E = new element_type(T) + elements += E + var/chosen_id = pick_n_take(left_ids) + E.puzzle_icon = puzzle_pieces["[chosen_id]"] + E.source = src + E.id = chosen_id + E.set_puzzle_icon() + + if(!is_solvable()) + make_solvable() + +/obj/structure/puzzle_element + name = "mysterious pillar" + desc = "puzzling..." + icon = 'icons/obj/lavaland/artefacts.dmi' + icon_state = "puzzle_pillar" + anchored = FALSE + density = TRUE + var/id = 0 + var/obj/effect/sliding_puzzle/source + var/icon/puzzle_icon + +/obj/structure/puzzle_element/Move(nloc, dir) + if(!isturf(nloc) || moving_diagonally || get_dist(get_step(src,dir),get_turf(source)) > 1) + return 0 + else + return ..() + +/obj/structure/puzzle_element/proc/set_puzzle_icon() + cut_overlays() + if(puzzle_icon) + //Need to scale it down a bit to fit the static border + var/icon/C = new(puzzle_icon) + C.Scale(19,19) + var/mutable_appearance/puzzle_small = new(C) + puzzle_small.layer = layer + 0.1 + puzzle_small.pixel_x = 7 + puzzle_small.pixel_y = 7 + add_overlay(puzzle_small) + +/obj/structure/puzzle_element/Destroy() + if(source) + source.elements -= src + source.validate() + return ..() + +//Set the full image on the turf and delete yourself +/obj/structure/puzzle_element/proc/collapse() + var/turf/T = get_turf(src) + var/mutable_appearance/MA = new(puzzle_icon) + MA.layer = T.layer + 0.1 + T.add_overlay(MA) + //Some basic shaking animation + for(var/i in 1 to COLLAPSE_DURATION) + animate(src, pixel_x=rand(-5,5), pixel_y=rand(-2,2), time=1) + QDEL_IN(src,COLLAPSE_DURATION) + +/obj/structure/puzzle_element/Moved() + . = ..() + source.validate() + +//Admin abuse version so you can pick the icon before it sets up +/obj/effect/sliding_puzzle/admin + auto_setup = FALSE + var/icon/puzzle_icon + var/puzzle_state + +/obj/effect/sliding_puzzle/admin/get_base_icon() + var/icon/I = new(puzzle_icon,puzzle_state) + return I + +//Ruin version +/obj/effect/sliding_puzzle/lavaland + reward_type = /obj/structure/closet/crate/necropolis/puzzle + +/obj/effect/sliding_puzzle/lavaland/dispense_reward() + if(prob(25)) + //If it's not roaming somewhere else already. + var/mob/living/simple_animal/hostile/megafauna/bubblegum/B = locate() in GLOB.mob_list + if(!B) + reward_type = /mob/living/simple_animal/hostile/megafauna/bubblegum + return ..() + +//Prison cube version +/obj/effect/sliding_puzzle/prison + auto_setup = FALSE //This will be done by cube proc + var/mob/living/prisoner + element_type = /obj/structure/puzzle_element/prison + +/obj/effect/sliding_puzzle/prison/get_base_icon() + if(!prisoner) + CRASH("Prison cube without prisoner") + prisoner.setDir(SOUTH) + var/icon/I = getFlatIcon(prisoner) + I.Scale(96,96) + return I + +/obj/effect/sliding_puzzle/prison/Destroy() + if(prisoner) + to_chat(prisoner,"With the cube broken by force, you can feel your body falling apart.") + prisoner.death() + qdel(prisoner) + . = ..() + +/obj/effect/sliding_puzzle/prison/dispense_reward() + prisoner.forceMove(get_turf(src)) + prisoner.notransform = FALSE + prisoner = null + +//Some armor so it's harder to kill someone by mistake. EDITED - Hugboxed +/obj/structure/puzzle_element/prison + resistance_flags = INDESTRUCTIBLE | ACID_PROOF | FIRE_PROOF | LAVA_PROOF + +/obj/structure/puzzle_element/prison/relaymove(mob/user) + return + +/obj/item/prisoncube + name = "Prison Cube" + desc = "Dusty cube with humanoid imprint on it." + icon = 'icons/obj/lavaland/artefacts.dmi' + icon_state = "prison_cube" + +/obj/item/prisoncube/afterattack(atom/target, mob/user, proximity_flag, click_parameters) + . = ..() + if(!proximity_flag || !isliving(target)) + return + var/mob/living/victim = target + var/mob/living/carbon/carbon_victim = victim + //Handcuffed or unconcious + if(istype(carbon_victim) && carbon_victim.handcuffed || victim.stat != CONSCIOUS) + if(!puzzle_imprison(target)) + to_chat(user,"[src] does nothing.") + return + to_chat(user,"You trap [victim] in the prison cube!") + qdel(src) + else + to_chat(user,"[src] only accepts restrained or unconcious prisoners.") + +/proc/puzzle_imprison(mob/living/prisoner) + var/turf/T = get_turf(prisoner) + var/obj/effect/sliding_puzzle/prison/cube = new(T) + if(!cube.check_setup_location()) + qdel(cube) + return FALSE + + //First grab the prisoner and move them temporarily into the generator so they won't get thrown around. + prisoner.notransform = TRUE + prisoner.forceMove(cube) + to_chat(prisoner,"You're trapped by the prison cube! You will remain trapped until someone solves it.") + + //Clear the area from objects (and cube user) + var/list/things_to_throw = list() + for(var/atom/movable/AM in range(1,T)) + if(!AM.anchored) + things_to_throw += AM + + for(var/atom/movable/AM in things_to_throw) + var/throwtarget = get_edge_target_turf(T, get_dir(T, get_step_away(AM, T))) + AM.throw_at(throwtarget, 2, 3) + + //Create puzzle itself + cube.prisoner = prisoner + cube.setup() + + //Move them into random block + var/obj/structure/puzzle_element/E = pick(cube.elements) + prisoner.forceMove(E) + return TRUE diff --git a/icons/obj/lavaland/artefacts.dmi b/icons/obj/lavaland/artefacts.dmi index 0530f7bb3b..7f11ba29d4 100644 Binary files a/icons/obj/lavaland/artefacts.dmi and b/icons/obj/lavaland/artefacts.dmi differ diff --git a/icons/obj/puzzle.dmi b/icons/obj/puzzle.dmi new file mode 100644 index 0000000000..f623142beb Binary files /dev/null and b/icons/obj/puzzle.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 5996f01dee..5f2fd794b0 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -2468,6 +2468,7 @@ #include "code\modules\research\xenobiology\crossbreeding\stabilized.dm" #include "code\modules\ruins\lavaland_ruin_code.dm" #include "code\modules\ruins\lavalandruin_code\biodome_clown_planet.dm" +#include "code\modules\ruins\lavalandruin_code\puzzle.dm" #include "code\modules\ruins\lavalandruin_code\sloth.dm" #include "code\modules\ruins\lavalandruin_code\surface.dm" #include "code\modules\ruins\objects_and_mobs\ash_walker_den.dm"