diff --git a/_maps/kalypso/plum.dmm b/_maps/kalypso/plum.dmm index 836d17d35ec..76a81cf86bb 100644 --- a/_maps/kalypso/plum.dmm +++ b/_maps/kalypso/plum.dmm @@ -27,7 +27,7 @@ /turf/open/floor/dirt, /area/template_noop) "m" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /mob/living/simple_animal/hostile/dragger, /turf/open/floor/naturalstone, /area/template_noop) diff --git a/_maps/map_files/austranderlin/austranderlin.dmm b/_maps/map_files/austranderlin/austranderlin.dmm index 1c6ae897a97..6723392ac05 100644 --- a/_maps/map_files/austranderlin/austranderlin.dmm +++ b/_maps/map_files/austranderlin/austranderlin.dmm @@ -32547,7 +32547,7 @@ /turf/open/floor/ruinedwood/darker, /area/outdoors/town/roofs) "oKx" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /turf/open/floor/cobblerock, /area/indoors/town/keep/magician) "oKG" = ( diff --git a/_maps/map_files/daftmarsh/daftmarsh.dmm b/_maps/map_files/daftmarsh/daftmarsh.dmm index 79c8c91872c..a21036483d9 100644 --- a/_maps/map_files/daftmarsh/daftmarsh.dmm +++ b/_maps/map_files/daftmarsh/daftmarsh.dmm @@ -17016,7 +17016,7 @@ /turf/closed/wall/mineral/stone/window, /area/indoors/town/church/chapel) "oLi" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /turf/open/floor/cobble, /area/under/town/basement) "oLy" = ( diff --git a/_maps/map_files/debug/roguetest.dmm b/_maps/map_files/debug/roguetest.dmm index 5fd81b6dd36..ef756477b6f 100644 --- a/_maps/map_files/debug/roguetest.dmm +++ b/_maps/map_files/debug/roguetest.dmm @@ -1086,9 +1086,9 @@ /turf/open/floor/grass/yel, /area/outdoors) "dO" = ( -/obj/machinery/essence/harvester, -/turf/open/floor/cobblerock, -/area/outdoors) +/obj/item/natural/infernalash, +/turf/open/floor/churchbrick, +/area/indoors/town/keep) "dP" = ( /obj/machinery/light/fueled/wallfire/candle{ pixel_x = 32; @@ -1096,6 +1096,10 @@ }, /turf/open/floor/tile, /area/indoors/town/keep) +"dT" = ( +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch, +/turf/open/floor/churchbrick, +/area) "dV" = ( /obj/machinery/light/fueled/torchholder/r, /turf/open/floor/churchbrick, @@ -1288,14 +1292,6 @@ /obj/structure/fluff/railing/wood, /turf/open/floor/dirt, /area/outdoors/mountains/decap) -"gM" = ( -/obj/structure/redstone/comparator{ - dir = 1; - pixel_y = 1; - mode = "subtract" - }, -/turf/open/floor/cobblerock, -/area) "hd" = ( /obj/structure/hotspring/border/five, /turf/open/floor/cobblerock, @@ -1317,6 +1313,10 @@ }, /turf/open/floor/tile/kitchen, /area/indoors/town/keep) +"hv" = ( +/obj/item/natural/feather, +/turf/open/floor/churchbrick, +/area/indoors/town/keep) "hF" = ( /obj/structure/dungeon_entry/center, /turf/open/floor/dirt, @@ -1456,6 +1456,11 @@ /obj/machinery/essence/test_tube, /turf/open/floor/wood, /area/indoors/town/keep) +"kf" = ( +/obj/machinery/light/fueled/torchholder/c, +/obj/item/spell_focus, +/turf/open/floor/churchbrick, +/area) "ki" = ( /obj/structure/rotation_piece/cog{ dir = 4 @@ -1629,6 +1634,10 @@ /obj/effect/landmark/stall, /turf/open/floor/grass/yel, /area/outdoors) +"nn" = ( +/obj/structure/mana_pylon, +/turf/open/floor/churchbrick, +/area/indoors/town/keep) "nt" = ( /obj/structure/chair/bench/couch/left{ dir = 4 @@ -1695,13 +1704,6 @@ /obj/effect/island_tester, /turf/closed/basic, /area/outdoors) -"pI" = ( -/obj/structure/redstone/comparator{ - dir = 2; - pixel_y = 1 - }, -/turf/open/floor/cobblerock, -/area) "pQ" = ( /obj/structure/rotation_piece, /turf/open/floor/wood, @@ -1840,10 +1842,6 @@ }, /turf/open/floor/grass/yel, /area/outdoors) -"rN" = ( -/obj/structure/redstone/dust, -/turf/open/floor/cobblerock, -/area) "rP" = ( /obj/machinery/light/fueled/wallfire/candle{ pixel_y = -32 @@ -1857,6 +1855,10 @@ "se" = ( /turf/closed/wall/mineral/decorstone, /area/indoors/town/keep) +"sl" = ( +/obj/item/clothing/ring/shimmeringlens, +/turf/open/floor/churchbrick, +/area/indoors/town/keep) "so" = ( /obj/structure/carpet, /turf/open/floor/tile/kitchen, @@ -1919,13 +1921,18 @@ }, /turf/closed/mineral/random/med, /area/outdoors/mountains/decap) +"tj" = ( +/obj/structure/mana_pylon, +/turf/open/floor/churchbrick, +/area) "tp" = ( /obj/structure/hotspring, /obj/effect/falling_sakura, /turf/open/floor/dirt/road, /area/outdoors) "tq" = ( -/obj/machinery/essence/extractor, +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/crop_growth, +/obj/item/mana_battery/mana_crystal/standard, /turf/open/floor/churchbrick, /area/indoors/town/keep) "tA" = ( @@ -2160,6 +2167,14 @@ "ya" = ( /obj/machinery/essence/splitter, /obj/structure/table/vtable, +/obj/item/essence_vial{ + pixel_y = 18; + pixel_x = -9 + }, +/obj/item/essence_vial{ + pixel_y = 14; + pixel_x = -4 + }, /turf/open/floor/wood, /area/indoors/town/keep) "yi" = ( @@ -2175,10 +2190,6 @@ /obj/machinery/light/fueled/torchholder/l, /turf/open/floor/wood, /area/indoors) -"yu" = ( -/obj/structure/redstone/pressure_plate, -/turf/open/floor/cobblerock, -/area) "yD" = ( /turf/open/dungeon_trap, /area/outdoors/mountains/decap) @@ -2220,13 +2231,6 @@ /obj/structure/water_vent, /turf/open/floor/carpet, /area/indoors) -"zw" = ( -/obj/structure/redstone/repeater{ - dir = 1; - delay_ticks = 1 - }, -/turf/open/floor/cobblerock, -/area) "zx" = ( /obj/structure/stairs/stone/reddish{ dir = 8 @@ -2439,10 +2443,6 @@ }, /turf/closed, /area/outdoors) -"zQ" = ( -/mob/living/carbon/human/species/kobold, -/turf/open/floor/cobblerock, -/area) "zT" = ( /obj/structure/chair/bench/couch/right{ dir = 4 @@ -2505,10 +2505,6 @@ /obj/structure/fake_machine/contractledger, /turf/open/floor/wood, /area/indoors/town/keep) -"Be" = ( -/obj/structure/floordoor, -/turf/open/floor/cobblerock, -/area) "Bp" = ( /obj/structure/chair/bench/couch/left{ dir = 8 @@ -2679,6 +2675,11 @@ "Fy" = ( /turf/closed/wall/mineral/decorstone, /area/indoors) +"Fz" = ( +/obj/item/mana_battery/mana_crystal/standard, +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/flight, +/turf/open/floor/churchbrick, +/area) "FN" = ( /obj/structure/flora/ausbushes/brflowers, /turf/open/floor/grass/yel, @@ -2788,6 +2789,10 @@ /obj/item/natural/chaff/oat, /turf/open/floor/blocks, /area/indoors/town/keep) +"HJ" = ( +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue, +/turf/open/floor/churchbrick, +/area) "HK" = ( /obj/structure/hotspring/border/two, /obj/machinery/light/fueled/torchholder/hotspring/standing{ @@ -2802,12 +2807,9 @@ /turf/open/openspace, /area/outdoors) "HQ" = ( -/obj/structure/redstone/repeater{ - dir = 2; - delay_ticks = 4 - }, -/turf/open/floor/cobblerock, -/area) +/obj/structure/soil/debug_soil/random, +/turf/open/floor/dirt/road, +/area/indoors/town/keep) "HR" = ( /obj/structure/fluff/walldeco/wantedposter/r, /turf/open/floor/cobble, @@ -2874,8 +2876,9 @@ /turf/open/floor/dirt/road, /area/outdoors) "IZ" = ( -/turf/closed/wall/mineral/stone, -/area) +/obj/item/natural/melded/t2, +/turf/open/floor/churchbrick, +/area/indoors/town/keep) "Jc" = ( /obj/structure/hotspring/border/seven, /obj/structure/bridge{ @@ -2884,6 +2887,13 @@ }, /turf/open/floor/dirt/road, /area/outdoors) +"Jd" = ( +/turf/closed/wall/mineral/stone, +/area) +"Je" = ( +/obj/item/pylon_linker, +/turf/open/floor/churchbrick, +/area/indoors/town/keep) "Jj" = ( /obj/structure/hotspring/border/eight, /turf/open/floor/cobblerock, @@ -3001,6 +3011,9 @@ /obj/item/bin, /turf/open/floor/cobblerock, /area/indoors/town/keep) +"Ll" = ( +/turf/open/floor/churchbrick, +/area) "Lm" = ( /mob/living/simple_animal/hostile/retaliate/trufflepig/male{ name = "GENERIC BLOCKER TESTER" @@ -3030,6 +3043,11 @@ dir = 1 }, /area/outdoors/mountains/decap) +"Lx" = ( +/obj/machinery/light/fueled/torchholder/c, +/obj/item/arcyne_spellobject/wand, +/turf/open/floor/churchbrick, +/area) "LX" = ( /mob/living/simple_animal/hostile/retaliate/banker, /turf/open/floor/wood, @@ -3069,12 +3087,9 @@ /obj/structure/gearbox, /turf/open/floor/dirt, /area/outdoors) -"MQ" = ( -/obj/structure/redstone/repeater{ - dir = 8; - delay_ticks = 1 - }, -/turf/open/floor/cobblerock, +"ML" = ( +/obj/item/spell_focus, +/turf/open/floor/churchbrick, /area) "MS" = ( /obj/structure/fluff/railing/wood{ @@ -3212,12 +3227,6 @@ }, /turf/open/floor/grass/yel, /area/outdoors) -"QV" = ( -/obj/structure/redstone/torch{ - dir = 2 - }, -/turf/open/floor/cobblerock, -/area) "Rc" = ( /obj/machinery/light/fueled/wallfire/candle{ pixel_x = 32; @@ -3285,6 +3294,10 @@ }, /turf/open/openspace, /area/outdoors) +"SC" = ( +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting, +/turf/open/floor/churchbrick, +/area/indoors/town/keep) "SE" = ( /obj/structure/hotspring/border/eleven, /obj/effect/lily_petal/three{ @@ -3406,9 +3419,6 @@ /obj/structure/flora/grass, /turf/open/floor/grass/mixyel, /area/outdoors/mountains/decap) -"Vh" = ( -/turf/open/floor/cobblerock, -/area) "Vk" = ( /turf/open/water/sewer, /area/outdoors) @@ -3655,12 +3665,6 @@ }, /turf/open/floor/blocks, /area/indoors/town/keep) -"XI" = ( -/obj/structure/redstone/torch{ - dir = 4 - }, -/turf/open/floor/cobblerock, -/area) "XL" = ( /obj/structure/hotspring/border/twelve, /turf/open/floor/dirt/road, @@ -3675,6 +3679,10 @@ }, /turf/open/floor/dirt/road, /area/outdoors) +"XX" = ( +/obj/machinery/light/fueled/torchholder/c, +/turf/open/floor/churchbrick, +/area) "Yf" = ( /obj/machinery/light/fueled/wallfire/candle{ pixel_x = 32; @@ -3726,11 +3734,9 @@ /obj/machinery/light/fueled/wallfire/candle, /turf/open/floor/blocks, /area/indoors/town/keep) -"Zn" = ( -/obj/structure/redstone/repeater{ - dir = 1 - }, -/turf/open/floor/cobblerock, +"Zr" = ( +/obj/item/arcyne_spellobject/spellstone/greater, +/turf/open/floor/churchbrick, /area) "Zs" = ( /turf/closed/wall/mineral/wood, @@ -3742,14 +3748,6 @@ /turf/open/floor/carpet, /area/indoors) "ZO" = ( -/obj/item/essence_vial{ - pixel_y = 18; - pixel_x = -9 - }, -/obj/item/essence_vial{ - pixel_y = 14; - pixel_x = -4 - }, /obj/item/essence_vial{ pixel_y = 8; pixel_x = 3 @@ -3759,8 +3757,9 @@ pixel_x = -6 }, /obj/structure/table/vtable/v2, -/obj/effect/spawner/map_spawner/random_lure, -/obj/effect/spawner/map_spawner/random_lure, +/obj/structure/chem_separator{ + pixel_y = 14 + }, /turf/open/floor/wood, /area/indoors/town/keep) @@ -4971,11 +4970,11 @@ ac ac ac ac -ac -ac -ac -ac -ac +as +as +as +as +as as as as @@ -5228,11 +5227,11 @@ ac ac ac ac -ac -ac -ac -ac -ac +as +as +as +as +as as as as @@ -5485,9 +5484,9 @@ ac ac ac ac -ac -ac -ac +as +as +as as as as @@ -5742,9 +5741,9 @@ ac ac ac ac -ac -ac -ac +as +as +as as as as @@ -5999,21 +5998,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -zQ -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -6256,21 +6255,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Be -rN -QV -IZ -Zn -rN -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -6513,21 +6512,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -6770,21 +6769,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Be -rN -Zn -IZ -Zn -rN -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -7027,21 +7026,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -7284,21 +7283,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Be -rN -rN -IZ -Zn -rN -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -7541,21 +7540,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -7798,21 +7797,21 @@ ac ac ac ac -ac -ac -ac as as as -Vh -Be -rN -rN -IZ -rN -rN -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -8055,21 +8054,21 @@ ac ac ac ac -ac -ac -ac as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -8312,21 +8311,21 @@ ac ac ac ac -ac -ac -ac as as -Vh -Vh -Be -Zn -rN -IZ -Zn -Zn -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -8574,16 +8573,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -8831,16 +8830,16 @@ as as as as -Vh -Vh -Be -Zn -rN -IZ -Zn -rN -yu -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -9088,16 +9087,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -9343,18 +9342,18 @@ as as as as -ac -ac -Vh -Be -Zn -rN -IZ -Zn -rN -Zn -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -9600,18 +9599,18 @@ as as as as -ac -ac -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -9857,18 +9856,18 @@ as as as as -ac -ac -Vh -Vh -Vh -Vh -Vh -QV -IZ -Zn -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -10114,18 +10113,18 @@ as as as as -ac -ac -Vh -rN -QV -IZ -Zn -rN -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -10371,18 +10370,18 @@ as as as as -ac -ac -Vh -Vh -Vh -Vh -Vh -QV -IZ -Zn -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -10628,18 +10627,18 @@ as as as as -ac -ac -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -10885,18 +10884,18 @@ as as as as -Vh -Vh -Vh -Vh -rN -rN -Zn -rN -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -11142,18 +11141,18 @@ as as as as -Vh -QV -IZ -Zn -rN -Vh -Vh -QV -IZ -Zn -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -11399,18 +11398,18 @@ as as as as -rN -rN -Vh -Vh -QV -IZ -Zn -rN -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as ac as as @@ -11656,18 +11655,18 @@ as as as as -Vh -QV -IZ -Zn -rN -Vh -Vh -QV -IZ -Zn -yu -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -11913,18 +11912,18 @@ as as as as -Vh -Vh -Vh -Vh -rN -rN -Zn -rN -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -12172,16 +12171,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -12429,16 +12428,16 @@ as as as as -Vh -Vh -Vh -Vh -rN -IZ -gM -rN -yu -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -12686,16 +12685,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -12943,16 +12942,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -rN -IZ -gM -yu -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -13200,16 +13199,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -13457,16 +13456,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -IZ -Zn -yu -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -13714,16 +13713,16 @@ as as as as -Vh -Be -IZ -Zn -rN -Vh -Vh -Vh -Vh -Vh +as +as +as +as +as +as +as +as +as +as as as as @@ -13971,16 +13970,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -IZ -Zn -yu -Vh +as +as +as +Jd +Jd +Jd +Jd +Jd +Jd +Jd as as as @@ -14228,16 +14227,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +Jd +kf +ML +ML +ML +ML +Jd as as as @@ -14485,16 +14484,16 @@ as as as as -Vh -Vh -Vh -Vh -rN -zw -XI -Vh -Vh -Vh +as +as +as +Jd +Ll +Ll +Ll +Ll +Ll +Jd as as as @@ -14742,16 +14741,16 @@ as as as as -Vh -Vh -Vh -Vh -rN -HQ -IZ -rN -Be -Vh +as +as +as +Jd +Ll +Ll +dT +Ll +Ll +Jd as as as @@ -14999,16 +14998,16 @@ as as as as -Vh -Vh -Vh -Vh -Vh -MQ -Vh -Vh -Vh -Vh +as +as +as +Jd +Ll +Ll +Ll +Ll +Ll +Jd as as as @@ -15256,21 +15255,21 @@ as as as as -Vh -Vh -Vh -Vh -Vh -QV -IZ -Zn -yu -Vh -as -as as as as +Jd +XX +Ll +Ll +Ll +Ll +Jd +zg +zg +zg +zg +zg as as zg @@ -15513,27 +15512,27 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -as -as as as as -as -as -as -at -tL -QN +Jd +Jd +Jd +Ll +Jd +Jd +zg +zg +HQ +HQ +HQ +zg +zg +zg +zg +zg +zg +zg zg ya GB @@ -15770,27 +15769,27 @@ as as as as -Vh -Vh -Vh -Vh -Vh -Vh -yu -Vh -Vh -Vh -as -as as as as -as -at -at -at -QN -Wr +Jd +XX +Ll +Ll +Ll +Ll +zg +wZ +vD +vD +vD +vD +zg +wZ +vD +vD +vD +vD zg ZO RP @@ -16027,27 +16026,27 @@ as as as as -Vh -Vh -Vh -Vh -Vh -IZ -gM -rN -yu -Vh -as -as -as as as as -at -QN -tL -QN -Wr +Jd +Ll +Ll +Ll +Ll +tj +EQ +vD +vD +vD +vD +nn +EQ +vD +vD +dO +vD +nn EQ fK GB @@ -16279,35 +16278,35 @@ ac ac ac ac -ac as as as as -Vh -Vh -Vh -Vh -Be -rN -pI -IZ -Vh -Vh as -ac -ac -ac -ac -ac -at -QN -QN -Wr -Wr -zg +as +as +as +Jd +Ll +Ll +Fz +Ll +Ll +vD +vD +vD tq vD +vD +vD +vD +hv +SC +IZ +vD +vD +vD +vD Ui oQ zg @@ -16536,32 +16535,32 @@ ac ac ac ac -ac as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh as -ac -ac -ac -ac -ac -at -Wr +as +as +as +Jd +Ll +Ll +Ll +Ll +Ll +zg +vD +vD +vD +vD +vD +zg +vD +vD dO -Wr -tL +vD +vD zg zg wZ @@ -16793,32 +16792,32 @@ ac ac ac ac -ac as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh as -ac -ac -ac -ac -ac -at -QN -QN -Wr -QN +as +as +as +Jd +XX +Ll +Ll +Ll +Ll +zg +wZ +vD +vD +vD +vD +zg +wZ +vD +vD +sl +Je zg ke GB @@ -17050,33 +17049,33 @@ ac ac ac ac -ac as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh as -ac -ac -ac -ac -ac -at -tL -QN -Wr -Wr -EQ +as +as +as +Jd +Jd +Jd +Ll +Jd +Jd +zg +zg +HQ +HQ +HQ +zg +zg +zg +zg +zg +zg +zg +zg AT GB vD @@ -17307,33 +17306,33 @@ ac ac ac ac -ac as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh as -ac -ac -ac -ac +as +as +as +Jd +XX +Ll +Ll +Ll +Ll +Jd +zg +zg +zg +zg +zg ac at QN QN tL Wr -zg +EQ kw GB vD @@ -17564,21 +17563,21 @@ ac ac ac ac -ac as as as as -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh -Vh +as +as +as +as +Jd +Ll +Ll +Ll +Ll +Ll +Jd as ac ac @@ -17821,13 +17820,6 @@ ac ac ac ac -ac -as -as -as -as -as -as as as as @@ -17836,6 +17828,13 @@ as as as as +Jd +Ll +Ll +HJ +Ll +Ll +Jd as ac ac @@ -18078,13 +18077,6 @@ ac ac ac ac -ac -as -as -as -as -as -as as as as @@ -18093,6 +18085,13 @@ as as as as +Jd +Ll +Ll +Ll +Ll +Ll +Jd as ac ac @@ -18335,13 +18334,6 @@ ac ac ac ac -ac -ac -ac -as -as -as -as as as as @@ -18350,6 +18342,13 @@ as as as as +Jd +Lx +Zr +Ll +Ll +Ll +Jd as ac ac @@ -18592,13 +18591,6 @@ ac ac ac ac -ac -ac -ac -ac -as -as -as as as as @@ -18607,6 +18599,13 @@ as as as as +Jd +Jd +Jd +Jd +Jd +Jd +Jd as as as @@ -18849,10 +18848,10 @@ ac ac ac ac -ac -ac -ac -ac +as +as +as +as as as as @@ -71022,9 +71021,9 @@ ac ac ac ac -ac -ac -ac +as +as +as as as as @@ -71279,9 +71278,9 @@ ac ac ac ac -ac -ac -ac +as +as +as as as as @@ -71536,20 +71535,20 @@ ac ac ac ac -ac -ac -ac as as -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -71793,20 +71792,277 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +"} +(11,1,2) = {" +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac ac ac ac as as -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -71820,11 +72076,11 @@ as ac ac ac -ac -ac -ac -ac -ac +as +as +as +as +as ac ac ac @@ -71870,10 +72126,7 @@ ac ac ac "} -(11,1,2) = {" -ac -ac -ac +(12,1,2) = {" ac ac ac @@ -72055,15 +72308,18 @@ ac ac as as -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -72127,10 +72383,7 @@ ac ac ac "} -(12,1,2) = {" -ac -ac -ac +(13,1,2) = {" ac ac ac @@ -72312,15 +72565,6 @@ ac ac as as -GE -GE -GE -GE -GE -GE -GE -GE -GE as as as @@ -72331,9 +72575,13 @@ as as as as -ac -ac -ac +as +as +as +as +as +as +as as as as @@ -72342,6 +72590,10 @@ as ac ac ac +as +as +ac +ac ac ac ac @@ -72383,12 +72635,13 @@ ac ac ac ac -"} -(13,1,2) = {" ac ac ac ac +"} +(14,1,2) = {" +ac ac ac ac @@ -72569,15 +72822,6 @@ ac ac as as -GE -GE -GE -GE -GE -GE -GE -GE -GE as as as @@ -72588,14 +72832,23 @@ as as as as -ac -ac -ac +as +as +as +as +as +as +as +as +as +as as as ac ac ac +as +as ac ac ac @@ -72623,10 +72876,13 @@ ac ac ac ac -ac -ac -ac -ac +ax +ax +ax +ax +ax +ax +ax ac ac ac @@ -72641,10 +72897,7 @@ ac ac ac "} -(14,1,2) = {" -ac -ac -ac +(15,1,2) = {" ac ac ac @@ -72826,15 +73079,18 @@ ac ac as as -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -72872,18 +73128,73 @@ ac ac ac ac +ax +ax +ax +ax +ax +ax +bt +am +cN +am +bW +ax +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +"} +(16,1,2) = {" +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac ac ac ac ac ac -ax -ax -ax -ax -ax -ax -ax ac ac ac @@ -72897,8 +73208,6 @@ ac ac ac ac -"} -(15,1,2) = {" ac ac ac @@ -73025,9 +73334,35 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +as ac ac ac @@ -73050,6 +73385,18 @@ ac ac ac ac +ax +am +am +cM +gj +ax +bu +am +bK +am +bX +ax ac ac ac @@ -73063,6 +73410,8 @@ ac ac ac ac +"} +(17,1,2) = {" ac ac ac @@ -73081,32 +73430,9 @@ ac ac ac ac -as -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as ac ac ac -as -as ac ac ac @@ -73129,18 +73455,6 @@ ac ac ac ac -ax -ax -ax -ax -ax -ax -bt -am -cN -am -bW -ax ac ac ac @@ -73154,8 +73468,6 @@ ac ac ac ac -"} -(16,1,2) = {" ac ac ac @@ -73279,9 +73591,35 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +as ac ac ac @@ -73304,6 +73642,18 @@ ac ac ac ac +ax +am +Lr +am +am +am +am +am +bL +am +bZ +ax ac ac ac @@ -73317,6 +73667,8 @@ ac ac ac ac +"} +(18,1,2) = {" ac ac ac @@ -73338,32 +73690,9 @@ ac ac ac ac -as -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as ac ac ac -as -as ac ac ac @@ -73386,18 +73715,6 @@ ac ac ac ac -ax -am -am -cM -gj -ax -bu -am -bK -am -bX -ax ac ac ac @@ -73411,8 +73728,6 @@ ac ac ac ac -"} -(17,1,2) = {" ac ac ac @@ -73533,9 +73848,35 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +as ac ac ac @@ -73558,6 +73899,53 @@ ac ac ac ac +ax +am +am +lV +HR +ax +bw +am +bM +bU +ca +ax +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +"} +(19,1,2) = {" +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac ac ac ac @@ -73595,32 +73983,9 @@ ac ac ac ac -as -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as ac ac ac -as -as ac ac ac @@ -73643,18 +74008,6 @@ ac ac ac ac -ax -am -Lr -am -am -am -am -am -bL -am -bZ -ax ac ac ac @@ -73668,8 +74021,6 @@ ac ac ac ac -"} -(18,1,2) = {" ac ac ac @@ -73754,9 +74105,35 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +as ac ac ac @@ -73779,6 +74156,18 @@ ac ac ac ac +ax +ax +ax +ax +ax +ax +bx +am +bN +bU +cb +ax ac ac ac @@ -73792,6 +74181,8 @@ ac ac ac ac +"} +(20,1,2) = {" ac ac ac @@ -73852,32 +74243,9 @@ ac ac ac ac -as -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as ac ac ac -as -as ac ac ac @@ -73900,18 +74268,6 @@ ac ac ac ac -ax -am -am -lV -HR -ax -bw -am -bM -bU -ca -ax ac ac ac @@ -73925,8 +74281,6 @@ ac ac ac ac -"} -(19,1,2) = {" ac ac ac @@ -74008,9 +74362,35 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +as ac ac ac @@ -74027,6 +74407,24 @@ ac ac ac ac +ax +ax +ax +ax +ax +ax +ax +ax +ax +ax +ax +ax +by +am +bO +am +bS +ax ac ac ac @@ -74040,6 +74438,13 @@ ac ac ac ac +"} +(21,1,2) = {" +ac +ac +ac +ac +ac ac ac ac @@ -74109,32 +74514,9 @@ ac ac ac ac -as -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as ac ac ac -as -as ac ac ac @@ -74157,18 +74539,6 @@ ac ac ac ac -ax -ax -ax -ax -ax -ax -bx -am -bN -bU -cb -ax ac ac ac @@ -74182,8 +74552,6 @@ ac ac ac ac -"} -(20,1,2) = {" ac ac ac @@ -74251,6 +74619,86 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +ac +ac +ac +as +as +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ax +ay +aH +aK +aN +aR +aW +ba +bf +bi +bl +ax +bz +am +bP +am +cc +ax +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +"} +(22,1,2) = {" +ac +ac ac ac ac @@ -74366,32 +74814,9 @@ ac ac ac ac -as -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as ac ac ac -as -as ac ac ac @@ -74408,24 +74833,6 @@ ac ac ac ac -ax -ax -ax -ax -ax -ax -ax -ax -ax -ax -ax -ax -by -am -bO -am -bS -ax ac ac ac @@ -74439,8 +74846,6 @@ ac ac ac ac -"} -(21,1,2) = {" ac ac ac @@ -74471,9 +74876,35 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +as ac ac ac @@ -74490,6 +74921,24 @@ ac ac ac ac +ax +az +aI +aI +aI +aI +aI +aI +aI +aI +aI +ax +bA +am +bQ +am +cd +ax ac ac ac @@ -74503,6 +74952,8 @@ ac ac ac ac +"} +(23,1,2) = {" ac ac ac @@ -74623,32 +75074,9 @@ ac ac ac ac -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as ac ac ac -as -as ac ac ac @@ -74665,54 +75093,6 @@ ac ac ac ac -ax -ay -aH -aK -aN -aR -aW -ba -bf -bi -bl -ax -bz -am -bP -am -cc -ax -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(22,1,2) = {" -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac ac ac ac @@ -74753,15 +75133,69 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +as ac ac ac ac ac ac +at +at +at +at +at +at +at +at +at +at +ax +aA +aI +aI +aI +aI +aI +aI +aI +aI +aI +ax +bB +am +cl +am +ce +ax ac ac ac @@ -74775,189 +75209,8 @@ ac ac ac ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as -ac -ac -ac -as -as -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ax -az -aI -aI -aI -aI -aI -aI -aI -aI -aI -ax -bA -am -bQ -am -cd -ax -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(23,1,2) = {" -ac -ac -ac +"} +(24,1,2) = {" ac ac ac @@ -75138,16 +75391,6 @@ ac ac ac as -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE as as as @@ -75158,253 +75401,9 @@ as as as as -ac -ac -ac as as -ac -ac -ac -ac -ac -ac -at -at -at -at -at -at -at -at -at -at -ax -aA -aI -aI -aI -aI -aI -aI -aI -aI -aI -ax -bB -am -cl -am -ce -ax -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(24,1,2) = {" -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac as -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE as as as @@ -75648,20 +75647,20 @@ ac ac ac ac -ac -ac -ac as -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -75905,20 +75904,20 @@ ac ac ac ac -ac -ac -ac as -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -76162,117 +76161,115 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +ac +ac +ac +as +as +at +at +QN +QN +Ue +QN +QN +qc +QN +ky +Pn +QN +ky +jC +hh +Vu +ax +aE +aI +aI +aP +aS +aY +bc +bh +bk +aD +ax +bE +am +bS +am +ch +ax +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +"} +(28,1,2) = {" +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac ac ac ac -as -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as -ac -ac -ac -as -as -at -at -QN -QN -Ue -QN -QN -qc -QN -ky -Pn -QN -ky -jC -hh -Vu -ax -aE -aI -aI -aP -aS -aY -bc -bh -bk -aD -ax -bE -am -bS -am -ch -ax -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(28,1,2) = {" -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac ac ac ac @@ -76421,18 +76418,20 @@ ac ac ac ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -76676,20 +76675,20 @@ ac ac ac ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -76933,116 +76932,69 @@ ac ac ac ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -as -as -as -as -as -as -as -as -as -as -ac -ac -ac -as -ab -Pq -am -Fo -am -cF -Kh -Qi -QN -QN -QN -QN -QN -QN -al -ai -QN -Wr -al -ai -Wr -Qi -QN -qc -ky -jC -QN -at -ax -ax -ax -ax -ax -ax -ax -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(31,1,2) = {" -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac +as +ab +Pq +am +Fo +am +cF +Kh +Qi +QN +QN +QN +QN +QN +QN +al +ai +QN +Wr +al +ai +Wr +Qi +QN +qc +ky +jC +QN +at +ax +ax +ax +ax +ax +ax +ax ac ac ac @@ -77056,6 +77008,8 @@ ac ac ac ac +"} +(31,1,2) = {" ac ac ac @@ -77192,18 +77146,63 @@ ac ac ac ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -77447,20 +77446,20 @@ ac ac ac ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -77704,20 +77703,20 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +as +as +as +as +as +as +as as as as @@ -77961,24 +77960,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -78218,24 +78217,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -78475,24 +78474,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -78732,24 +78731,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -78989,24 +78988,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -rN -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -79246,24 +79245,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -IZ -Zn -rN -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -79503,139 +79502,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -rN -GE -GE -GE -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -at -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -Sz -ss -ss -ss -ss -ss -ss -at -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(41,1,2) = {" -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -79647,6 +79531,33 @@ ac ac ac ac +at +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +Sz +ss +ss +ss +ss +ss +ss +at ac ac ac @@ -79667,6 +79578,8 @@ ac ac ac ac +"} +(41,1,2) = {" ac ac ac @@ -79764,16 +79677,6 @@ ac ac ac ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE ac ac ac @@ -79789,33 +79692,6 @@ ac ac ac ac -at -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -Sz -ss -ss -ss -ss -ss -ss -at ac ac ac @@ -79836,8 +79712,6 @@ ac ac ac ac -"} -(42,1,2) = {" ac ac ac @@ -79885,6 +79759,24 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -79896,6 +79788,33 @@ ac ac ac ac +at +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +Sz +ss +ss +ss +ss +ss +ss +at ac ac ac @@ -79916,6 +79835,9 @@ ac ac ac ac +"} +(42,1,2) = {" +ac ac ac ac @@ -80021,16 +79943,6 @@ ac ac ac ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE ac ac ac @@ -80046,33 +79958,6 @@ ac ac ac ac -at -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -Sz -ss -ss -ss -ss -ss -at -at ac ac ac @@ -80093,8 +79978,6 @@ ac ac ac ac -"} -(43,1,2) = {" ac ac ac @@ -80133,10 +80016,62 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +ac +ac +ac +ac +ac +ac +ac ac ac ac ac +at +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +Sz +ss +ss +ss +ss +ss +at +at ac ac ac @@ -80157,6 +80092,8 @@ ac ac ac ac +"} +(43,1,2) = {" ac ac ac @@ -80278,16 +80215,6 @@ ac ac ac ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE ac ac ac @@ -80303,32 +80230,6 @@ ac ac ac ac -at -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -Sz -ss -ss -ss -ss -ss -at ac ac ac @@ -80350,8 +80251,6 @@ ac ac ac ac -"} -(44,1,2) = {" ac ac ac @@ -80374,6 +80273,24 @@ ac ac ac ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -80385,6 +80302,32 @@ ac ac ac ac +at +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +Sz +ss +ss +ss +ss +ss +at ac ac ac @@ -80406,6 +80349,17 @@ ac ac ac ac +"} +(44,1,2) = {" +ac +ac +ac +ac +ac +ac +ac +ac +ac ac ac ac @@ -80535,16 +80489,6 @@ ac ac ac ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE ac ac ac @@ -80560,6 +80504,61 @@ ac ac ac ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac at at at @@ -80788,24 +80787,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -81045,20 +81044,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE +as +as +as +as +as +as +as +Jd +Jd +Jd +Jd +Jd +Jd +Jd +as +as +as +as ac ac ac @@ -81068,10 +81071,6 @@ ac ac ac ac -GE -GE -ac -ac ac ac ac @@ -81302,88 +81301,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac -ac -ac -ac -ac -ac -GE -GE -ac -ac -ac -ac -ac -ac -at -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -Sz -ss -ss -ss -ss -ss -ss -at -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(48,1,2) = {" -ac -ac -ac -ac +as +as +as +as +as +as +Jd +Jd +GE +GE +GE +GE +GE +Jd +as +as +as +as ac ac ac @@ -81397,6 +81332,31 @@ ac ac ac ac +at +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +Sz +ss +ss +ss +ss +ss +ss +at ac ac ac @@ -81417,6 +81377,8 @@ ac ac ac ac +"} +(48,1,2) = {" ac ac ac @@ -81563,16 +81525,57 @@ ac ac ac ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +as +as +as +as +as +as +Jd +Ll GE GE GE GE GE -GE -GE -GE -GE -GE +Jd +as +as +as +as ac ac ac @@ -81580,10 +81583,6 @@ ac ac ac ac -GE -GE -GE -GE ac ac ac @@ -81816,20 +81815,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE +as +as +as +as +as +as +Jd +Ll GE GE GE GE GE +Jd +as +as +as +as ac ac ac @@ -81837,10 +81840,6 @@ ac ac ac ac -GE -GE -GE -GE ac ac ac @@ -82073,20 +82072,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE +as +as +as +as +as +as +Jd +Ll GE GE GE GE GE +Jd +as +as +as +as ac ac ac @@ -82094,10 +82097,6 @@ ac ac ac ac -GE -GE -GE -GE ac ac ac @@ -82330,20 +82329,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE +as +as +as +as +as +as +Jd +Jd GE GE GE GE GE +Jd +as +as +as +as ac ac ac @@ -82351,10 +82354,6 @@ ac ac ac ac -GE -GE -GE -GE ac ac ac @@ -82587,108 +82586,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac -ac -ac -ac -GE -GE -GE -GE -ac -ac -ac -ac -ac -ac -Zs -oB -oB -Zs -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -Sz -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -ss -at -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -"} -(53,1,2) = {" -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac +as +as +as +as +as +as +as +Jd +Jd +Jd +Jd +Jd +Jd +Jd +as +as +as +as ac ac ac @@ -82702,6 +82617,36 @@ ac ac ac ac +Zs +oB +oB +Zs +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +Sz +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +ss +at ac ac ac @@ -82717,6 +82662,20 @@ ac ac ac ac +"} +(53,1,2) = {" +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac ac ac ac @@ -82848,16 +82807,6 @@ ac ac ac ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE ac ac ac @@ -82865,6 +82814,56 @@ ac ac ac ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +ac +ac +ac GE GE GE @@ -83101,24 +83100,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -83358,24 +83357,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -83615,24 +83614,24 @@ ac ac ac ac -ac -ac -ac -ac -GE -GE -GE -GE -GE -GE -GE -GE -GE -GE -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac @@ -83872,24 +83871,24 @@ ac ac ac ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac -ac +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as +as ac ac ac diff --git a/_maps/map_files/rosewood/rosewood_mountain.dmm b/_maps/map_files/rosewood/rosewood_mountain.dmm index 8ec67d314ca..6600f74aa01 100644 --- a/_maps/map_files/rosewood/rosewood_mountain.dmm +++ b/_maps/map_files/rosewood/rosewood_mountain.dmm @@ -282,7 +282,7 @@ /turf/open/floor/cobble, /area/under/mountains/anvil/lower) "cx" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/max, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/max, /turf/open/floor/grass/red, /area/under/mountains/anvil/dungeon/lower) "cy" = ( @@ -424,7 +424,7 @@ /turf/open/floor/carpet/red, /area/under/mountains/anvil/dungeon/lower) "dy" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/adv, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/adv, /mob/living/simple_animal/hostile/retaliate/infernal/watcher, /turf/open/floor/herringbone, /area/under/mountains/anvil/dungeon/lower) @@ -1587,7 +1587,7 @@ /turf/open/floor/naturalstone, /area/under/mountains/anvil/lower) "mD" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/teleport, +/obj/effect/decal/cleanable/ritual_rune/arcyne/teleport, /turf/open/floor/herringbone, /area/under/mountains/anvil/dungeon) "mG" = ( @@ -2224,7 +2224,7 @@ /turf/open/floor/church, /area/under/mountains/anvil/lower) "rQ" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /mob/living/simple_animal/hostile/retaliate/elemental/warden{ name = "Guardian Warden" }, @@ -6317,7 +6317,7 @@ /turf/open/floor/ruinedwood/turned/darker, /area/indoors/mountains/anvil/surface/building) "Yo" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /obj/effect/spawner/map_spawner/loot/common, /mob/living/simple_animal/hostile/retaliate/elemental/warden{ name = "Guardian Warden" @@ -6363,7 +6363,7 @@ /turf/open/floor/carpet/purple, /area/under/mountains/anvil/dungeon/lower) "YG" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /mob/living/simple_animal/hostile/retaliate/elemental/warden{ name = "Guardian Warden" }, diff --git a/_maps/map_files/vanderlin/vanderlin.dmm b/_maps/map_files/vanderlin/vanderlin.dmm index 2ed70070d89..efcfd130716 100644 --- a/_maps/map_files/vanderlin/vanderlin.dmm +++ b/_maps/map_files/vanderlin/vanderlin.dmm @@ -25437,6 +25437,9 @@ pixel_x = -7; pixel_y = 17 }, +/obj/structure/chem_separator{ + pixel_y = 17 + }, /turf/open/floor/blocks/stonered/tiny, /area/indoors/town/clinic_large/apothecary) "mbG" = ( @@ -31325,7 +31328,7 @@ /turf/open/floor/ruinedwood/darker, /area/outdoors/town/roofs) "oKx" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /turf/open/floor/cobblerock, /area/indoors/town/keep/magician) "oKG" = ( diff --git a/_maps/map_files/vanderlin/vanderlin_bog.dmm b/_maps/map_files/vanderlin/vanderlin_bog.dmm index e695131c897..e765814d240 100644 --- a/_maps/map_files/vanderlin/vanderlin_bog.dmm +++ b/_maps/map_files/vanderlin/vanderlin_bog.dmm @@ -18996,7 +18996,7 @@ }, /area/under/tomb/indoors/magic) "snE" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid{ +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid{ pixel_x = -18; pixel_y = -16 }, diff --git a/_maps/map_files/vanderlin/vanderlin_forest.dmm b/_maps/map_files/vanderlin/vanderlin_forest.dmm index 372b3ac76f0..9259539e25b 100644 --- a/_maps/map_files/vanderlin/vanderlin_forest.dmm +++ b/_maps/map_files/vanderlin/vanderlin_forest.dmm @@ -5113,7 +5113,7 @@ /turf/open/floor/wood, /area/indoors/wilderness/gallowband/ship) "ED" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /mob/living/simple_animal/hostile/retaliate/elemental/behemoth, /obj/effect/decal/cleanable/dirt, /turf/open/floor/cobble, diff --git a/_maps/map_files/vanderlin/vanderlin_mountain.dmm b/_maps/map_files/vanderlin/vanderlin_mountain.dmm index dfbf274f898..26b5af5eef3 100644 --- a/_maps/map_files/vanderlin/vanderlin_mountain.dmm +++ b/_maps/map_files/vanderlin/vanderlin_mountain.dmm @@ -2540,7 +2540,7 @@ /obj/effect/landmark/hammer/dev_text{ name = "Cool dragon fight" }, -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/max, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/max, /turf/open/floor/naturalstone, /area/under/mountains/anvil/lower) "ama" = ( @@ -5510,9 +5510,9 @@ pixel_x = -7; name = " defunct Magical Artillery device" }, -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, -/obj/effect/decal/cleanable/roguerune/arcyne/wallgreater, -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/adv, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/wallgreater, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/adv, /turf/open/floor/cobble, /area/indoors/mountains/anvil/keep) "azX" = ( @@ -6323,7 +6323,7 @@ /turf/open/floor/metal/barograte, /area/indoors/town/clocktower) "aDz" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/leylines, +/obj/effect/decal/cleanable/ritual_rune/arcyne/leylines, /obj/item/weapon/hammer/sledgehammer/war/malum, /turf/open/floor/naturalstone, /area/under/mountains/anvil/lower) @@ -7037,7 +7037,7 @@ /turf/open/floor/herringbone, /area/indoors/mountains/anvil/upperkeep) "aHj" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /turf/open/floor/naturalstone, /area/under/mountains/anvil/lower) "aHk" = ( @@ -7796,7 +7796,7 @@ /turf/open/floor/snow/patchy, /area/outdoors/mountains/anvil/snowyforest) "aKT" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/empowerment{ +/obj/effect/decal/cleanable/ritual_rune/arcyne/empowerment{ pixel_y = -47 }, /turf/open/floor/naturalstone, @@ -8105,8 +8105,8 @@ /turf/open/floor/carpet/royalblack, /area/indoors/town/church/inquisition) "aMp" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/adv, -/obj/effect/decal/cleanable/roguerune/arcyne/attunement, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/adv, +/obj/effect/decal/cleanable/ritual_rune/arcyne/attunement, /obj/item/riddleofsteel, /mob/living/simple_animal/hostile/retaliate/voiddragon/red, /turf/open/floor/plasteel/maniac, @@ -11113,7 +11113,7 @@ /turf/open/floor/concrete, /area/under/mountains/anvil/upper) "bKV" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/enchantment, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /turf/open/floor/cobble, /area/indoors/mountains/anvil/upperkeep) "bRz" = ( @@ -11890,7 +11890,7 @@ /obj/structure/fluff/railing/tall/stone{ dir = 4 }, -/obj/effect/decal/cleanable/roguerune/arcyne/knowledge, +/obj/effect/decal/cleanable/ritual_rune/arcyne/knowledge, /turf/open/floor/cobblerock, /area/indoors/mountains/anvil/upperkeep) "fRO" = ( @@ -12800,7 +12800,7 @@ /turf/open/floor/blocks/newstone/alt, /area/indoors/mountains/anvil/upperkeep) "lkK" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/leylines, +/obj/effect/decal/cleanable/ritual_rune/arcyne/leylines, /obj/machinery/essence/enchantment_altar, /obj/effect/decal/cleanable/debris/stone, /turf/open/floor/cobble, diff --git a/_maps/map_files/whitepalacepass/WhitePalacePass.dmm b/_maps/map_files/whitepalacepass/WhitePalacePass.dmm index b1e84179fa8..5d717f158b2 100644 --- a/_maps/map_files/whitepalacepass/WhitePalacePass.dmm +++ b/_maps/map_files/whitepalacepass/WhitePalacePass.dmm @@ -33423,7 +33423,7 @@ /turf/open/floor/carpet/red, /area/indoors/town/keep/halls/w) "vuv" = ( -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid, +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid, /mob/living/simple_animal/hostile/retaliate/elemental/warden, /turf/open/floor/blocks, /area/under/town/basement) diff --git a/code/__DEFINES/magic.dm b/code/__DEFINES/magic.dm index 566eea16d06..0cca77abb76 100644 --- a/code/__DEFINES/magic.dm +++ b/code/__DEFINES/magic.dm @@ -167,6 +167,8 @@ DEFINE_BITFIELD(antimagic_flags, list( #define SPELL_TEMPORARY (1 << 3) +#define SPELL_UNETCHABLE (1 << 4) + // Bitflags for spell requirements /// Whether the spell requires wizard clothes to cast. #define SPELL_REQUIRES_WIZARD_GARB (1 << 0) @@ -207,3 +209,26 @@ DEFINE_BITFIELD(spell_requirements, list( * as it encompasses more states in which a mob may be "incorporeal from magic" */ #define is_jaunting(atom) (istype(atom.loc, /obj/effect/dummy/phased_mob)) + +/// When set, the item hijacks afterattack and fires a spell via diceroll +/// rather than passively granting spells to the holder. +#define SPELLOBJECT_HIJACK_CLICK (1<<0) +/// When set (alongside HIJACK_CLICK), aim-failure picks a random nearby mob +/// instead of always backfiring onto the user, and spell names are obscured in examine. +#define SPELLOBJECT_CHAOTIC (1<<1) +/// When set, the item deletes itself when all spell charges are exhausted +#define SPELLOBJECT_CONSUMABLE (1<<2) +/// If set update overlays adds a small version to the object +#define SPELLOBJECT_VISUAL (1<<3) +/// If set we will always fire true +#define SPELLOBJECT_STABLE (1<<4) + +/// Diceroll requirement at each arcane skill tier for aimed-fire +/// Lower = easier to hit the intended target (roll-under system) +#define SPELLOBJECT_AIM_REQ_NONE 6 +#define SPELLOBJECT_AIM_REQ_NOVICE 9 +#define SPELLOBJECT_AIM_REQ_APPRENTICE 11 +#define SPELLOBJECT_AIM_REQ_JOURNEYMAN 13 +#define SPELLOBJECT_AIM_REQ_EXPERT 15 +#define SPELLOBJECT_AIM_REQ_MASTER 16 +#define SPELLOBJECT_AIM_REQ_LEGENDARY 17 diff --git a/code/__DEFINES/medical.dm b/code/__DEFINES/medical.dm index 50acd6ed0fd..210d8497c25 100644 --- a/code/__DEFINES/medical.dm +++ b/code/__DEFINES/medical.dm @@ -268,11 +268,11 @@ DEFINE_BITFIELD(organ_flags, list( /// We need to take at least this much brainloss gained at once to roll for brain traumas, any less it won't roll #define TRAUMA_ROLL_THRESHOLD 4.5 /// Brainloss caused by mildly low blood oxygenation -#define BRAIN_DAMAGE_LOW_OXYGENATION 1.1 +#define BRAIN_DAMAGE_LOW_OXYGENATION 1.5 /// Brainloss caused by lower than low blood oxygenation -#define BRAIN_DAMAGE_LOWER_OXYGENATION 2.2 +#define BRAIN_DAMAGE_LOWER_OXYGENATION 3 /// Brainloss caused by a complete lack of oxygen flow -#define BRAIN_DAMAGE_LOWEST_OXYGENATION 3.3 +#define BRAIN_DAMAGE_LOWEST_OXYGENATION 4.5 // ~pulse levels, very simplified. #define PULSE_NONE 0 // So !M.pulse checks would be possible. @@ -308,7 +308,7 @@ DEFINE_BITFIELD(organ_flags, list( // ~arteries #define ARTERY_MAX_HEALTH 100 -#define ARTERIAL_BLOOD_FLOW 3 +#define ARTERIAL_BLOOD_FLOW 10 #define ARTERY_HEAD /obj/item/organ/artery/head #define ARTERY_MOUTH /obj/item/organ/artery/mouth diff --git a/code/__DEFINES/pain.dm b/code/__DEFINES/pain.dm index df52dbd2ddb..aa23811a184 100644 --- a/code/__DEFINES/pain.dm +++ b/code/__DEFINES/pain.dm @@ -6,14 +6,14 @@ #define PAIN_LEVEL_4 70 // ~shock stages -#define SHOCK_STAGE_1 30 -#define SHOCK_STAGE_2 90 -#define SHOCK_STAGE_3 120 -#define SHOCK_STAGE_4 180 // "Softcrit" -#define SHOCK_STAGE_5 240 -#define SHOCK_STAGE_6 360 // "Hardcrit" -#define SHOCK_STAGE_7 450 -#define SHOCK_STAGE_8 600 +#define SHOCK_STAGE_1 10 +#define SHOCK_STAGE_2 30 +#define SHOCK_STAGE_3 40 +#define SHOCK_STAGE_4 60 // "Softcrit" +#define SHOCK_STAGE_5 80 +#define SHOCK_STAGE_6 120 // "Hardcrit" +#define SHOCK_STAGE_7 150 +#define SHOCK_STAGE_8 200 #define SHOCK_STAGE_MAX SHOCK_STAGE_8 // ~shock modifiers diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm index f88d9425b2b..826d368e57b 100644 --- a/code/__DEFINES/traits/definitions.dm +++ b/code/__DEFINES/traits/definitions.dm @@ -278,6 +278,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_BLOCKED_DIAGONAL "blocked_diagonals" /// Can swim ignoring water flow and slowdown #define TRAIT_SWIMMER "Good Swimmer" +///can we ride the lightning +#define TRAIT_PYLON_RIDER "Pylon Rider" /// trait determines if this mob can breed given by /datum/component/breeding #define TRAIT_MOB_BREEDER "mob_breeder" /// can't be perceived in any way, likely due to invisibility @@ -504,6 +506,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_BETTER_SLEEP "Better Sleep" //Recover more energy (blue bar) when sleeping #define TRAIT_EXTEROCEPTION "Exteroception" //See others' hunger and thirst #define TRAIT_TUTELAGE "Tutelage" //Slightly more sleep xp to you and xp to apprentices +#define TRAIT_ARCANE_KNOWLEDGE "Arcane Knowledge" #define TRAIT_APRICITY "Apricity" //Decreased stamina regen time during DAY #define TRAIT_BLACKLEG "Blackleg" //Rig coin, dice, cards in your favor #define TRAIT_INQUISITION "Member of the Oratorium Throni Vacui" diff --git a/code/_globalvars/lists/magic.dm b/code/_globalvars/lists/magic.dm new file mode 100644 index 00000000000..dc3635506dc --- /dev/null +++ b/code/_globalvars/lists/magic.dm @@ -0,0 +1,66 @@ +GLOBAL_LIST_INIT(rune_types, generate_rune_types_for_tier(null)) +GLOBAL_LIST_INIT(t1rune_types, generate_rune_types_for_tier(1)) +GLOBAL_LIST_INIT(t2rune_types, generate_rune_types_for_tier(2)) +GLOBAL_LIST_INIT(t3rune_types, generate_rune_types_for_tier(3)) +GLOBAL_LIST_INIT(t4rune_types, generate_rune_types_for_tier(4)) +/// List of all teleport runes currently active in the world +GLOBAL_LIST(teleport_runes) + + +/// Builds an assoc list of [rune.name] = typepath for all scribable runes. +/// Pass max_tier = null to include every tier (used for the admin/debug list). +/proc/generate_rune_types_for_tier(max_tier) + RETURN_TYPE(/list) + var/list/runes = list() + for(var/obj/effect/decal/cleanable/ritual_rune/rune as anything in subtypesof(/obj/effect/decal/cleanable/ritual_rune)) + if(!initial(rune.can_be_scribed)) + continue + if(!isnull(max_tier) && initial(rune.tier) > max_tier) + continue + runes[initial(rune.name)] = rune + return runes + +GLOBAL_LIST_INIT(allowedrunerituallist, generate_ritual_list(/datum/runerituals, exclude_types = list(/datum/runerituals/summoning, /datum/runerituals/wall))) + +GLOBAL_LIST_INIT(t1summoningrunerituallist, generate_ritual_list(/datum/runerituals/summoning, max_tier = 1)) +GLOBAL_LIST_INIT(t2summoningrunerituallist, generate_ritual_list(/datum/runerituals/summoning, max_tier = 2)) +GLOBAL_LIST_INIT(t3summoningrunerituallist, generate_ritual_list(/datum/runerituals/summoning, max_tier = 3)) +GLOBAL_LIST_INIT(t4summoningrunerituallist, generate_ritual_list(/datum/runerituals/summoning)) + +GLOBAL_LIST_INIT(t2wallrunerituallist, generate_ritual_list(/datum/runerituals/wall, max_tier = 2)) +GLOBAL_LIST_INIT(t4wallrunerituallist, generate_ritual_list(/datum/runerituals/wall, min_tier = 3)) + +GLOBAL_LIST_INIT(buffrunerituallist, generate_ritual_list(/datum/runerituals/buff, max_tier = 1)) +GLOBAL_LIST_INIT(t2buffrunerituallist, generate_ritual_list(/datum/runerituals/buff)) + +/** + * Builds an assoc list of [ritual.name] = typepath from subtypes of root_type. + * + * Arguments: + * * root_type - The parent type to search under (uses subtypesof). + * * max_tier - If set, skips rituals with tier > max_tier. + * * min_tier - If set, skips rituals with tier < min_tier. + * * exclude_types - List of typepaths; rituals that are or inherit from any of these are skipped. + * Uses istype() so child types are also excluded. + */ +/proc/generate_ritual_list(root_type, max_tier = null, min_tier = null, list/exclude_types = null) + RETURN_TYPE(/list) + var/list/out = list() + for(var/datum/runerituals/R as anything in subtypesof(root_type)) + if(initial(R.blacklisted)) + continue + var/r_tier = initial(R.tier) + if(!isnull(max_tier) && r_tier > max_tier) + continue + if(!isnull(min_tier) && r_tier < min_tier) + continue + if(length(exclude_types)) + var/excluded = FALSE + for(var/exc in exclude_types) + if(istype(R, exc)) + excluded = TRUE + break + if(excluded) + continue + out[initial(R.name)] = R + return out diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm index 5815ebfa740..1c24aad3f5d 100644 --- a/code/_onclick/hud/rendering/plane_master.dm +++ b/code/_onclick/hud/rendering/plane_master.dm @@ -258,7 +258,7 @@ appearance_flags = PLANE_MASTER blend_mode = BLEND_OVERLAY //render_target = GAME_PLANE_RENDER_TARGET - render_relay_plane = null + render_relay_plane = GAME_PLANE /atom/movable/screen/plane_master/leylines/backdrop(mob/mymob) . = ..() diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 18382bcc1ac..f092510d286 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -160,11 +160,15 @@ /atom/movable/screen/craft/Click(location, control, params) var/list/modifiers = params2list(params) if(modifiers["middle"]) - if(QDELETED(book)) - book = new(null) var/mob/M = usr for(var/datum/recipe as anything in M.mind?.learned_recipes) book.types |= recipe.type + var/datum/job/job = SSjob.GetJob(M.job) + if(job && !book) + book = new job.book_type(null) + else if(QDELETED(book)) + book = new(null) + book.ui_interact(usr) return if(world.time < lastclick + 3 SECONDS) diff --git a/code/datums/attributes/_experience.dm b/code/datums/attributes/_experience.dm index 2b798c8558e..b6d50c73237 100644 --- a/code/datums/attributes/_experience.dm +++ b/code/datums/attributes/_experience.dm @@ -185,6 +185,8 @@ GLOBAL_VAR_INIT(sleep_experience_modifier, 1.0) multiplier += 0.05 if(parent?.has_quirk(/datum/quirk/boon/quick_learner)) multiplier += 0.2 + if(HAS_TRAIT(parent, TRAIT_ARCANE_KNOWLEDGE)) + multiplier += 0.4 // Catchup multiplier: boost XP when skill is below its default-attribute floor var/datum/attribute/skill/skill = GET_ATTRIBUTE_DATUM(skill_type) diff --git a/code/datums/enchantments/on_hit/_base.dm b/code/datums/enchantments/on_hit/_base.dm index 07aa240ca18..77723c8938a 100644 --- a/code/datums/enchantments/on_hit/_base.dm +++ b/code/datums/enchantments/on_hit/_base.dm @@ -14,6 +14,15 @@ registered_signals += COMSIG_GLOVES_POST_ATTACK_HAND RegisterSignal(item, COMSIG_GLOVES_POST_ATTACK_HAND, PROC_REF(on_attackhand_delegate)) + registered_signals += COMSIG_PROJECTILE_ON_HIT + RegisterSignal(item, COMSIG_PROJECTILE_ON_HIT, PROC_REF(on_projectile_delegate)) + +/datum/enchantment/on_hit/proc/on_projectile_delegate(obj/item/source, mob/living/attacker, mob/living/attacked, angle, def_zone, damage) + if(ishuman(attacked)) + on_human_attack(source, attacked, attacker, attacker.get_bodypart(def_zone), damage) + else + on_simple_attack(source, attacked, attacker, damage) + /datum/enchantment/on_hit/proc/on_attackhand_delegate(obj/item/source, mob/living/attacked, mob/living/attacker, damage) if(ishuman(attacked)) var/zone_hit = attacker.zone_selected diff --git a/code/datums/enchantments/pylon_rider.dm b/code/datums/enchantments/pylon_rider.dm new file mode 100644 index 00000000000..5de15891111 --- /dev/null +++ b/code/datums/enchantments/pylon_rider.dm @@ -0,0 +1,33 @@ +/datum/enchantment/pylon_rider + enchantment_name = "Pylon Rider" + examine_text = "It pulses with leyline energy, as if eager to carry you along the flow of mana." + enchantment_end_message = "The leyline attunement fades from the item." + enchantment_color = COLOR_CYAN + essence_recipe = list( + /datum/thaumaturgical_essence/magic = 35, + /datum/thaumaturgical_essence/air = 15, + ) + required_type = list(/obj/item/clothing) + var/active_item + +/datum/enchantment/pylon_rider/register_triggers(atom/item) + . = ..() + registered_signals += COMSIG_ITEM_EQUIPPED + RegisterSignal(item, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equip)) + registered_signals += COMSIG_ITEM_DROPPED + RegisterSignal(item, COMSIG_ITEM_DROPPED, PROC_REF(on_drop)) + +/datum/enchantment/pylon_rider/proc/on_equip(obj/item/i, mob/living/user, slot) + if((slot & ITEM_SLOT_HANDS) && !ismobholder(i)) + return + if(active_item) + return + active_item = TRUE + ADD_TRAIT(user, TRAIT_PYLON_RIDER, "[REF(i)]") + to_chat(user, span_notice("You feel attuned to the leylines.")) + +/datum/enchantment/pylon_rider/proc/on_drop(obj/item/i, mob/living/user) + if(active_item) + active_item = FALSE + REMOVE_TRAIT(user, TRAIT_PYLON_RIDER, "[REF(i)]") + to_chat(user, span_notice("You feel disconnected from the leylines.")) diff --git a/code/datums/injury/_injury.dm b/code/datums/injury/_injury.dm index eba885797a0..c45688a56dd 100644 --- a/code/datums/injury/_injury.dm +++ b/code/datums/injury/_injury.dm @@ -358,7 +358,7 @@ // unbandages the injury /datum/injury/proc/unbandage_injury() - injury_flags |= INJURY_BANDAGED + injury_flags &= ~INJURY_BANDAGED return TRUE /datum/injury/proc/is_bleeding() @@ -379,7 +379,7 @@ for(var/obj/item/item in embedded_objects) if((item.w_class < WEIGHT_CLASS_SMALL)) bad_embeddies += 1 - return max(0.1, (bleed_rate * damage)/20 + bad_embeddies) + return max(0.1, (bleed_rate * damage)/10 + bad_embeddies) /datum/injury/proc/is_surgical() if(CHECK_BITFIELD(injury_flags, INJURY_SURGICAL)) diff --git a/code/datums/mana/mana_battery.dm b/code/datums/mana/mana_battery.dm index 1d8fe6e3431..8dfcacfb1a6 100644 --- a/code/datums/mana/mana_battery.dm +++ b/code/datums/mana/mana_battery.dm @@ -71,11 +71,8 @@ name = MAGIC_MATERIAL_NAME + " crystal" desc = "Crystalized mana." //placeholder desc icon = 'icons/obj/crystals.dmi' //placeholder - -/obj/item/mana_battery/mana_crystal/Initialize(mapload) - . = ..() - create_reagents(40) - reagents.add_reagent(/datum/reagent/toxin/plasma, 40) + grind_results = list(/datum/reagent/toxin/plasma = 40) + indexed = TRUE // Do not use, basetype /datum/mana_pool/mana_battery/mana_crystal @@ -223,6 +220,15 @@ amount = 0 ethereal_recharge_rate = 0 intrinsic_recharge_sources = MANA_ALL_LEYLINES + var/transfer_threshold = 0 + +/datum/mana_pool/mana_pylon/transfer_specific_mana(datum/mana_pool/other_pool, amount_to_transfer, decrement_budget = TRUE) + if(istype(other_pool, /datum/mana_pool/mana_pylon)) + if(amount - amount_to_transfer < transfer_threshold) + amount_to_transfer = max(0, amount - transfer_threshold) + if(!amount_to_transfer) + return 0 + return ..() /datum/mana_pool/mana_fountain maximum_mana_capacity = 1000 diff --git a/code/datums/rituals/ritual.dm b/code/datums/rituals/ritual.dm deleted file mode 100644 index c4a9f649297..00000000000 --- a/code/datums/rituals/ritual.dm +++ /dev/null @@ -1,436 +0,0 @@ -GLOBAL_LIST_INIT(runeritualslist, generate_runeritual_types()) -GLOBAL_LIST_INIT(allowedrunerituallist, generate_allowed_runeritual_types()) -GLOBAL_LIST_INIT(t1summoningrunerituallist, generate_t1summoning_rituallist()) -GLOBAL_LIST_INIT(t2summoningrunerituallist, generate_t2summoning_rituallist()) -GLOBAL_LIST_INIT(t3summoningrunerituallist, generate_t3summoning_rituallist()) -GLOBAL_LIST_INIT(t4summoningrunerituallist, generate_t4summoning_rituallist()) -GLOBAL_LIST_INIT(t2wallrunerituallist, generate_t2wall_rituallist()) -GLOBAL_LIST_INIT(t4wallrunerituallist, generate_t4wall_rituallist()) -GLOBAL_LIST_INIT(buffrunerituallist, generate_buff_rituallist()) -GLOBAL_LIST_INIT(t2buffrunerituallist, generate_t2buff_rituallist()) -/proc/generate_runeritual_types() //debug list - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in typesof(/datum/runerituals)) - runerituals[initial(runeritual.name)] = runeritual - return runerituals -/proc/generate_allowed_runeritual_types() //list of all non-summoning rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals)) - if(istype(runeritual, /datum/runerituals/summoning || /datum/runerituals/wall)) - continue - if(runeritual.blacklisted) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_t1summoning_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals/summoning)) - if(runeritual.tier > 1) - continue - if(runeritual.blacklisted) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_t2summoning_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals/summoning)) - if(runeritual.tier > 2) - continue - if(runeritual.blacklisted) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_t3summoning_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals/summoning)) - if(runeritual.tier > 3) - continue - if(runeritual.blacklisted) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_t4summoning_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals/summoning)) - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_t2wall_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in typesof(/datum/runerituals/wall)) - if(runeritual.tier > 2) - continue - if(runeritual.blacklisted) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_t4wall_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals/wall)) - if(runeritual.tier < 3) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_buff_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals/buff)) - if(runeritual.tier > 1) - continue - if(runeritual.blacklisted) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/proc/generate_t2buff_rituallist() //list of all rituals for player use - RETURN_TYPE(/list) - var/list/runerituals = list() - for(var/datum/runerituals/runeritual as anything in subtypesof(/datum/runerituals/buff)) - if(runeritual.blacklisted) - continue - runerituals[initial(runeritual.name)] = runeritual - return runerituals - -/datum/runerituals - abstract_type = /datum/runerituals - var/category = "Rituals" - var/name - var/desc - var/list/required_atoms = list() - var/list/result_atoms = list() - var/list/banned_atom_types = list() - var/mob_to_summon - var/blacklisted = FALSE - var/tier = 0 /// Tier var is used for 'tier' of ritual, if the ritual has tiers. EX: Summoning rituals. If it doesn't have tiers, set tier to 0. - -/datum/runerituals/proc/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - if(!length(result_atoms)) - return FALSE - - for(var/result in result_atoms) - new result(loc) - return TRUE - -/datum/runerituals/proc/parse_required_item(atom/item_path, number_of_things) - // If we need a human, there is a high likelihood we actually need a (dead) body - if(ispath(item_path, /mob/living/carbon/human)) - return "bod[number_of_things > 1 ? "ies" : "y"]" - if(ispath(item_path, /mob/living)) - return "carcass[number_of_things > 1 ? "es" : ""] of any kind" - return "[initial(item_path.name)]\s" - -/** - * Called after on_finished_recipe returns TRUE - * and a ritual was successfully completed. - * - * Goes through and cleans up (deletes) - * all atoms in the selected_atoms list. - * - * Remove atoms from the selected_atoms - * (either in this proc or in on_finished_recipe) - * to NOT have certain atoms deleted on cleanup. - * - * Arguments - * * selected_atoms - a list of all atoms we intend on destroying. - */ -/datum/runerituals/proc/cleanup_atoms(list/selected_atoms) - SHOULD_CALL_PARENT(TRUE) - - for(var/atom/sacrificed as anything in selected_atoms) - if(isliving(sacrificed)) - continue - - selected_atoms -= sacrificed - qdel(sacrificed) - - - -/datum/runerituals/buff - blacklisted = TRUE - tier = 1 - var/buff - -/datum/runerituals/knowledge - name = "knowledge gain" - tier = 1 - blacklisted = FALSE - required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 1) - -/datum/runerituals/knowledge/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - return TRUE -/datum/runerituals/leyattunement - name = "leyline attunement" - tier = 1 - blacklisted = FALSE - required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 1,/obj/item/reagent_containers/food/snacks/produce/manabloom = 2,/obj/item/natural/leyline = 1) - -/datum/runerituals/leyattunement/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - return TRUE - - -/datum/runerituals/buff/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - return TRUE - -/datum/runerituals/buff/strength - name = "arcane augmentation of strength" - buff = /datum/status_effect/buff/magicstrength - tier = 2 - blacklisted = FALSE - required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 2,/obj/item/natural/elementalshard = 2) - -/datum/runerituals/buff/lesserstrength - name = "lesser arcane augmentation of strength" - buff = /datum/status_effect/buff/magicstrength/lesser - blacklisted = FALSE - required_atoms = list(/obj/item/natural/elementalmote = 2,/obj/item/mana_battery/mana_crystal/small = 1) - -/datum/runerituals/buff/constitution - name = "fortify constitution" - buff = /datum/status_effect/buff/magicconstitution - tier = 2 - blacklisted = FALSE - required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 2, /obj/item/natural/obsidian = 4) - -/datum/runerituals/buff/lesserconstitution - name = "lesser fortify constitution" - buff = /datum/status_effect/buff/magicconstitution/lesser - blacklisted = FALSE - required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 1, /obj/item/natural/obsidian = 2) - -/datum/runerituals/buff/speed - name = "haste" - buff = /datum/status_effect/buff/magicspeed - tier = 2 - blacklisted = FALSE - required_atoms = list(/obj/item/natural/artifact = 2, /obj/item/natural/leyline = 2) - -/datum/runerituals/buff/lesserspeed - name = "lesser haste" - buff = /datum/status_effect/buff/magicspeed/lesser - blacklisted = FALSE - required_atoms = list(/obj/item/natural/artifact = 1, /obj/item/natural/leyline = 1) - -/datum/runerituals/buff/perception - name = "arcane eyes" - buff = /datum/status_effect/buff/magicperception - tier = 2 - blacklisted = FALSE - required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 2, /obj/item/natural/hellhoundfang = 1) - -/datum/runerituals/buff/lesserperception - name = "lesser arcane eyes" - buff = /datum/status_effect/buff/magicperception/lesser - blacklisted = FALSE - required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 1, /obj/item/natural/infernalash = 2) - -/datum/runerituals/buff/endurance - name = "vitalized endurance" - buff = /datum/status_effect/buff/magicendurance - tier = 2 - blacklisted = FALSE - required_atoms = list(/obj/item/natural/obsidian = 2, /obj/item/natural/iridescentscale = 1) - -/datum/runerituals/buff/lesserendurance - name = "lesser vitalized endurance" - buff = /datum/status_effect/buff/magicendurance/lesser - blacklisted = FALSE - required_atoms = list(/obj/item/natural/obsidian = 1, /obj/item/natural/fairydust = 2) - -/datum/runerituals/buff/nightvision - name = "darksight" - buff = /datum/status_effect/buff/darkvision - blacklisted = FALSE - required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 2, /obj/item/natural/iridescentscale = 1, /obj/item/natural/elementalshard = 1) - -/datum/runerituals/wall - name = "lesser arcyne wall" - tier = 1 - blacklisted = FALSE - required_atoms = list(/obj/item/natural/elementalmote = 2, /obj/item/mana_battery/mana_crystal/small = 1, /obj/item/natural/melded/t1 = 1) - -/datum/runerituals/wall/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - return 1 - -/datum/runerituals/wall/t2 - name = "greater arcyne wall" - tier = 2 - required_atoms = list(/obj/item/natural/elementalmote = 4, /obj/item/mana_battery/mana_crystal/small = 2, /obj/item/natural/melded/t1 = 1) - -/datum/runerituals/wall/t2/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - return 2 - -/datum/runerituals/wall/t3 - name = "arcyne fortress" - tier = 3 - required_atoms = list(/obj/item/natural/artifact = 3, /obj/item/mana_battery/mana_crystal/small = 3, /obj/item/natural/melded/t3 = 1) - - -/datum/runerituals/attunement - name = "arcyne attunement" - required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 1, /obj/item/natural/melded/t1 = 1) - - var/list/attunement_modifiers = list() - var/list/attuned_items = list() - -/datum/runerituals/attunement/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - for(var/obj/item/item in selected_atoms) - if(!length(item.attunement_values)) - continue - for(var/attunement in item.attunement_values) - attunement_modifiers |= attunement - attunement_modifiers[attunement] += item.attunement_values[attunement] - attuned_items |= item - -/datum/runerituals/teleport - name = "planar convergence" - tier = 3 - required_atoms = list(/obj/item/natural/artifact = 1, /obj/item/natural/leyline = 1, /obj/item/natural/melded/t2 = 1) - -/datum/runerituals/teleport/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - return TRUE - -////////////////SUMMONING RITUALS/////////////////// -/datum/runerituals/summoning - abstract_type = /datum/runerituals/summoning - name = "summoning ritual parent" - desc = "summoning parent rituals." - blacklisted = TRUE - - - -/datum/runerituals/summoning/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - return summon_ritual_mob(user, loc, mob_to_summon) - -/datum/runerituals/summoning/proc/summon_ritual_mob(mob/living/user, turf/loc, mob/living/mob_to_summon) - var/mob/living/simple_animal/summoned - if(isliving(mob_to_summon)) - summoned = mob_to_summon - else - summoned = new mob_to_summon(loc) - ADD_TRAIT(summoned, TRAIT_PACIFISM, MAGIC_TRAIT) //can't kill while planar bound. - summoned.status_flags += GODMODE//It's not meant to be killable until released from its planar binding. - summoned.binded = TRUE //No auto movement, no moving to targets - summoned.SetParalyzed(90 SECONDS) - summoned.candodge = FALSE - animate(summoned, color = "#ff0000",time = 5) - return summoned - - - -/datum/runerituals/summoning/imp - name = "summoning lesser infernal" - desc = "summons an infernal imp" - blacklisted = FALSE - tier = 1 - required_atoms = list(/obj/item/fertilizer/ash = 2, /obj/item/natural/obsidian = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/imp//temporary rat 4 testing - -/datum/runerituals/summoning/hellhound - name = "summoning hellhound" - desc = "summons a hellhound" - blacklisted = FALSE - tier = 2 - required_atoms = list(/obj/item/natural/infernalash = 3, /obj/item/natural/obsidian = 1, /obj/item/natural/melded/t1 = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/hellhound//temporary rat 4 testing - -/datum/runerituals/summoning/watcher - name = "summoning infernal watcher" - desc = "summons an infernal watcher" - blacklisted = FALSE - tier = 3 - required_atoms = list(/obj/item/natural/hellhoundfang = 2, /obj/item/natural/obsidian = 1, /obj/item/natural/melded/t2 =1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/watcher//temporary rat 4 testing - -/datum/runerituals/summoning/archfiend - name = "summoning fiend" - desc = "summons a fiend" - blacklisted = FALSE - tier = 4 - required_atoms = list(/obj/item/natural/moltencore = 1, /obj/item/natural/obsidian = 3, /obj/item/natural/melded/t3 =1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/fiend//temporary rat 4 testing - -/datum/runerituals/summoning/sprite - name = "summoning sprite" - desc = "summons a fae sprite" - blacklisted = FALSE - tier = 1 - required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 1, /obj/item/reagent_containers/food/snacks/produce/fruit/jacksberry = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/sprite - -/datum/runerituals/summoning/glimmer - name = "summoning glimmerwing" - desc = "summons a fae spirit" - blacklisted = FALSE - tier = 2 - required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 1, /obj/item/natural/fairydust = 3, /obj/item/natural/melded/t1 = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/glimmerwing - -/datum/runerituals/summoning/dryad - name = "summoning dryad" - desc = "summons a drayd" - blacklisted = FALSE - tier = 3 - required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 2, /obj/item/natural/iridescentscale = 2, /obj/item/natural/melded/t2 = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/dryad - -/datum/runerituals/summoning/sylph - name = "summoning sylph" - desc = "summons an archfae" - blacklisted = FALSE - tier = 4 - required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 1, /obj/item/natural/heartwoodcore = 1, /obj/item/natural/melded/t3 = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/sylph - -/datum/runerituals/summoning/crawler - name = "summoning elemental crawler" - desc = "summons a minor elemental" - blacklisted = FALSE - tier = 1 - required_atoms = list(/obj/item/natural/stone = 3, /obj/item/mana_battery/mana_crystal/small = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/crawler - -/datum/runerituals/summoning/warden - name = "summoning elemental warden" - desc = "summons an elemental" - blacklisted = FALSE - tier = 2 - required_atoms = list(/obj/item/natural/elementalmote = 3, /obj/item/mana_battery/mana_crystal/small = 1, /obj/item/natural/melded/t1 = 1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/warden - -/datum/runerituals/summoning/behemoth - name = "summoning elemental behemoth" - desc = "summons a large elemental" - blacklisted = FALSE - tier = 3 - required_atoms = list(/obj/item/natural/elementalshard = 2, /obj/item/mana_battery/mana_crystal/small = 1, /obj/item/natural/melded/t2 =1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/behemoth - -/datum/runerituals/summoning/collossus - name = "summoning elemental colossus" - desc = "summons a huge elemental" - blacklisted = FALSE - tier = 4 - required_atoms = list(/obj/item/natural/elementalfragment = 1, /obj/item/mana_battery/mana_crystal/small = 1, /obj/item/natural/melded/t3 =1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/collossus - -/datum/runerituals/summoning/abberant - name = "summoning aberrant from the void" - desc = "summons a long forgotten creature" - blacklisted = FALSE - tier = 4 - required_atoms = list(/obj/item/natural/melded/t5 =1) - mob_to_summon = /mob/living/simple_animal/hostile/retaliate/voiddragon diff --git a/code/datums/rituals/ritual_runes.dm b/code/datums/rituals/ritual_runes.dm deleted file mode 100644 index c1c47711f2b..00000000000 --- a/code/datums/rituals/ritual_runes.dm +++ /dev/null @@ -1,892 +0,0 @@ -/obj/effect/decal/cleanable/roguerune // basis for all rituals - name = "ritualrune" - desc = "Strange symbols pulse upon the ground..." - anchored = TRUE - icon = 'icons/obj/rune.dmi' - icon_state = "6" - resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF - obj_flags = NONE - layer = SIGIL_LAYER - color = null - var/magictype = "arcyne"//"arcyne", "divine", "druid", "blood" - var/runesize = 0 //Used to determine range of 'range' of the rune when counting for invokers. Should correspond to rune size. EX: 32x32 and 96x96 should be size 1. Increase each size by one for every tile radius increase after. - var/invoker_name = "basic rune" - - /// The description of the ritual rune shown to those who have knowledge to examine it - var/invoker_desc = "a basic rune with no function." - - /// This is said by those when the rune is invoked. - var/invocation = "Aiy ele-mayo!" - /// The amount of invokers required around the rune to invoke it. - var/req_invokers = 1 - - /// If we have a description override for required invokers to invoke - var/req_invokers_text - /// Used for some runes, this is for when you want a rune to not be usable when in use. - var/rune_in_use = FALSE - - /// Used when you want to keep track of who erased the rune - var/log_when_erased = FALSE - /// If has more then one ritual associated, TRUE. Else, FALSE - var/ritual_number = FALSE - /// Whether this rune can be scribed or if it's admin only / special spawned / whatever - var/can_be_scribed = TRUE - /// How long the rune takes to erase - var/erase_time = 1.5 SECONDS - /// How long the rune takes to create - var/scribe_delay = 4 SECONDS - /// If a rune cannot be speed boosted while scribing on certain turfs - var/no_scribe_boost = FALSE - /// If a rune provides a bonus to a spell, or spellbook reading. - var/spellbonus = 0 - /// Hhow much damage you take doing it - var/scribe_damage = 5 - /// How much damage invokers take when invoking it - var/invoke_damage = 0 - /// If the rune requires a keyword when scribed - var/req_keyword = FALSE - /// The actual keyword for the rune - var/keyword - /// Global proc to call while the rune is being created - var/started_creating - /// Global proc to call if the rune fails to be created - var/failed_to_create - var/active = FALSE - /// Tier var is used for 'tier' of rune, if the rune has tiers. EX: Summoning runes. If it doesn't have tiers, set tier to 0. - var/tier = 1 - /// ritual result is the result of a ritual! - var/ritual_result - //atoms in ranges - var/list/atom/movable/atoms_in_range //list for atoms in range of rune - var/datum/runerituals/pickritual //selected - var/list/selected_atoms - var/associated_ritual = null //Associated ritual for runes with only 1 ritual. Use in tandom with ritual_number - var/takes_all_items = FALSE - -/proc/isarcyne(mob/living/carbon/human/A) - return istype(A) && A.mind && (GET_MOB_SKILL_VALUE(A, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) //checks if person has arcane skill - -/proc/isdivine(mob/living/carbon/human/A) - return istype(A) && A.mind && (GET_MOB_SKILL_VALUE(A, /datum/attribute/skill/magic/holy) > SKILL_LEVEL_NONE) //checks if person has holy/divine skill - -/proc/isdruid(mob/living/carbon/human/A) - return istype(A) && A.mind && (GET_MOB_SKILL_VALUE(A, /datum/attribute/skill/magic/druidic) > SKILL_LEVEL_NONE) //checks if person has druidic skill - -/proc/isblood(mob/living/carbon/human/A) - return istype(A) && A.mind && (GET_MOB_SKILL_VALUE(A, /datum/attribute/skill/magic/blood) > SKILL_LEVEL_NONE) //checks if person has blood magic skill - -GLOBAL_LIST_INIT(rune_types, generate_rune_types()) -GLOBAL_LIST_INIT(t1rune_types, generate_t1rune_types()) -GLOBAL_LIST_INIT(t2rune_types, generate_t2rune_types()) -GLOBAL_LIST_INIT(t3rune_types, generate_t3rune_types()) -GLOBAL_LIST_INIT(t4rune_types, generate_t4rune_types()) - -/// List of all teleport runes -GLOBAL_LIST(teleport_runes) - -/// Returns an associated list of rune types. [rune.cultist_name] = [typepath] -/proc/generate_rune_types() - RETURN_TYPE(/list) - var/list/runes = list() - for(var/obj/effect/decal/cleanable/roguerune/rune as anything in subtypesof(/obj/effect/decal/cleanable/roguerune)) - if(!initial(rune.can_be_scribed)) - continue - runes[initial(rune.name)] = rune // Uses the invoker name for displaying purposes - return runes - -/proc/generate_t1rune_types() - RETURN_TYPE(/list) - var/list/runes = list() - for(var/obj/effect/decal/cleanable/roguerune/rune as anything in subtypesof(/obj/effect/decal/cleanable/roguerune)) - if(rune.tier > 1) - continue - if(!initial(rune.can_be_scribed)) - continue - runes[initial(rune.name)] = rune // Uses the invoker name for displaying purposes - return runes - -/proc/generate_t2rune_types() - RETURN_TYPE(/list) - var/list/runes = list() - for(var/obj/effect/decal/cleanable/roguerune/rune as anything in subtypesof(/obj/effect/decal/cleanable/roguerune)) - if(rune.tier > 2) - continue - if(!initial(rune.can_be_scribed)) - continue - runes[initial(rune.name)] = rune // Uses the invoker name for displaying purposes - return runes - -/proc/generate_t3rune_types() - RETURN_TYPE(/list) - var/list/runes = list() - for(var/obj/effect/decal/cleanable/roguerune/rune as anything in subtypesof(/obj/effect/decal/cleanable/roguerune)) - if(rune.tier > 3) - continue - if(!initial(rune.can_be_scribed)) - continue - runes[initial(rune.name)] = rune // Uses the invoker name for displaying purposes - return runes - -/proc/generate_t4rune_types() - RETURN_TYPE(/list) - var/list/runes = list() - for(var/obj/effect/decal/cleanable/roguerune/rune as anything in subtypesof(/obj/effect/decal/cleanable/roguerune)) - if(!initial(rune.can_be_scribed)) - continue - runes[initial(rune.name)] = rune // Uses the invoker name for displaying purposes - return runes - - -/obj/effect/decal/cleanable/roguerune/Initialize(mapload, set_keyword) - . = ..() - if(set_keyword) - keyword = set_keyword - -/obj/effect/decal/cleanable/roguerune/proc/do_invoke_glow() - set waitfor = FALSE - animate(src, transform = matrix()*2, alpha = 0, time = 5, flags = ANIMATION_END_NOW) //fade out - sleep(0.5 SECONDS) - animate(src, transform = matrix(), alpha = 255, time = 0, flags = ANIMATION_END_NOW) - -/obj/effect/decal/cleanable/roguerune/proc/fail_invoke() - //This proc contains the effects of a rune if it is not invoked correctly, through either invalid wording or not enough cultists. By default, it's just a basic fizzle. - visible_message(span_warning("The markings pulse with a small flash of light, then fall dark.")) - var/oldcolor = color - color = rgb(255, 0, 0) - animate(src, color = oldcolor, time = 5) - addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_atom_colour)), 0.5 SECONDS) - rune_in_use = FALSE - -/obj/effect/decal/cleanable/roguerune/attack_hand(mob/living/user) - if(rune_in_use) - to_chat(user, span_notice("Someone is already using this rune.")) - return - if(.) - return - if(!ritual_number ) //Only one option of ritual for this rune - var/list/invokers = can_invoke(user) - if(length(invokers) >= req_invokers) //Enough invokers? If Yes, invoke - invoke(invokers) - else - to_chat(user, span_danger("You need [req_invokers - length(invokers)] more adjacent invokers to use this rune in such a manner.")) //Needs more invokers, fails invoke - fail_invoke() - else - var/list/invokers = can_invoke(user) - if(length(invokers) >= req_invokers) - var/list/rituals = list() - if(istype(src,/obj/effect/decal/cleanable/roguerune/arcyne/summoning)) - var/tier = src.tier - if(tier >= 4) - rituals += GLOB.t4summoningrunerituallist - else if(tier == 3) - rituals += GLOB.t3summoningrunerituallist - else if(tier == 2) - rituals += GLOB.t2summoningrunerituallist - else if(tier == 1) - rituals += GLOB.t1summoningrunerituallist - else if(istype(src,/obj/effect/decal/cleanable/roguerune/arcyne/wall)) - var/tier = src.tier - if(tier >= 3) - rituals += GLOB.t4wallrunerituallist - else - rituals += GLOB.t2wallrunerituallist - else if(istype(src,/obj/effect/decal/cleanable/roguerune/arcyne/empowerment)) - var/tier = src.tier - if(tier == 1) - rituals += GLOB.buffrunerituallist - else - rituals+= GLOB.t2buffrunerituallist - else if(istype(src,/obj/effect/decal/cleanable/roguerune/arcyne)) - rituals += GLOB.allowedrunerituallist - var/ritualnameinput = input(user, "Rituals", "Vanderlin") as null|anything in rituals - var/datum/runerituals/pickritual1 - pickritual1 = rituals[ritualnameinput] - if(!pickritual1 || pickritual1 == null) - rune_in_use = FALSE - return - if(pickritual1.tier > src.tier) - to_chat(user, span_hierophant_warning("Your ritual rune is not strong enough to perform this ritual.")) - return - invoke(invokers, pickritual1) - else - to_chat(user, span_danger("You need [req_invokers - length(invokers)] more adjacent invokers to use this rune in such a manner.")) //Needs more invokers, fails invoke - fail_invoke() - . = ..() - - -/obj/effect/decal/cleanable/roguerune/proc/can_invoke(mob/living/user=null) - rune_in_use = TRUE - //This proc determines if the rune can be invoked at the time. If there are multiple required invokers, it will find all nearby invokers. - var/list/invokers = list() //people eligible to invoke the rune - if(user) - invokers += user - if(req_invokers > 1) - for(var/mob/living/invoker in range(runesize, src)) - if(!invoker.can_speak()) - continue - if(invoker.stat != CONSCIOUS) - continue - if(magictype == "arcyne") - if(isarcyne(invoker)) - invokers += invoker - if(magictype == "divine") - if(isdivine(invoker)) - invokers += invoker - if(magictype == "druid") - if(isdruid(invoker)) - invokers += invoker - if(magictype == "blood") - if(isblood(invoker)) - invokers += invoker - if(invoker == user) - continue - - return invokers - -/obj/effect/decal/cleanable/roguerune/proc/invoke(list/invokers, datum/runerituals/runeritual) //Generic invoke proc. This will be defined on every rune, along with effects.If you want to make an object, or provide a buff, do so through this proc., have both here. - rune_in_use = FALSE - atoms_in_range = list() - for(var/atom/close_atom as anything in range(runesize, src)) - if(!ismovable(close_atom)) - continue - if(isitem(close_atom)) - var/obj/item/close_item = close_atom - if(close_item.item_flags & ABSTRACT) //woops sacrificed your own head - continue - if(close_atom.invisibility) - continue - if(close_atom == usr) - continue - if(close_atom == src) - continue - atoms_in_range += close_atom - pickritual = new runeritual - if(!islist(pickritual.required_atoms)) - to_chat(invokers, span_notice("required atoms is NOT a list")) //debug message. Remove later. - - // A copy of our requirements list. - // We decrement the values of to determine if enough of each required item is present. - - var/list/requirements_list = pickritual.required_atoms.Copy() - var/list/banned_atom_types = pickritual.banned_atom_types.Copy() - // A list of all atoms we've selected to use in this recipe. - selected_atoms = list() - for(var/atom/nearby_atom as anything in atoms_in_range) - // Go through all of our required atoms - for(var/req_type in requirements_list) - // We already have enough of this type, skip - if(requirements_list[req_type] <= 0) - continue - // If req_type is a list of types, check all of them for one match. - if(islist(req_type)) - if(!(is_type_in_list(nearby_atom, req_type))) - continue - else if(!istype(nearby_atom, req_type)) - continue - // if list has items, check if the strict type is banned. - if(length(banned_atom_types)) - if(nearby_atom.type in banned_atom_types) - continue - // This item is a valid type. Add it to our selected atoms list. - selected_atoms |= nearby_atom - requirements_list[req_type]-- - - if(takes_all_items && isitem(nearby_atom)) - if(length(nearby_atom:attunement_values)) - selected_atoms |= nearby_atom - - var/list/what_are_we_missing = list() - for(var/req_type in requirements_list) - var/number_of_things = requirements_list[req_type] - // <= 0 means it's fulfilled, skip - if(number_of_things <= 0) - continue - // > 0 means it's unfilfilled - the ritual has failed, we should tell them why - // Lets format the thing they're missing and put it into our list - var/formatted_thing = "[number_of_things] " - if(islist(req_type)) - var/list/req_type_list = req_type - var/list/req_text_list = list() - for(var/atom/possible_type as anything in req_type_list) - req_text_list += pickritual.parse_required_item(possible_type) - formatted_thing += english_list(req_text_list, and_text = "or") - - else - formatted_thing = pickritual.parse_required_item(req_type) - - what_are_we_missing += formatted_thing - if(length(what_are_we_missing)) - // Let them know it screwed up - to_chat(usr, span_hierophant_warning("Ritual failed, missing components!")) - // Then let them know what they're missing - to_chat(usr, span_hierophant_warning("You are missing [english_list(what_are_we_missing)] in order to complete the ritual \"[pickritual.name]\".")) - fail_invoke() - return FALSE - - playsound(usr, 'sound/magic/teleport_diss.ogg', 75, TRUE) - - ritual_result = pickritual.on_finished_recipe(usr, selected_atoms, loc) - - return TRUE - -/obj/effect/decal/cleanable/roguerune/arcyne //arcane - name = "arcane ritual rune" - desc = "subtype used for arcane rituals- you should not be seeing this." - magictype = "arcyne" - can_be_scribed = FALSE - -/obj/effect/decal/cleanable/roguerune/arcyne/attack_hand(mob/living/user) - if(!isarcyne(user)) - to_chat(user, span_warning("You aren't able to understand the words of [src].")) - return - . = ..() - -/obj/effect/decal/cleanable/roguerune/arcyne/knowledge //used for better quality of learning, grants temporary 2 minute INT bonus. - name = "Knowledge rune" - desc = "arcane symbols pulse upon the ground..." - icon_state = "6" - invocation = "Thal’miren vek’laris un’vethar!" - color = "#3A0B61" - spellbonus = 15 - scribe_damage = 10 - can_be_scribed = TRUE - associated_ritual = /datum/runerituals/knowledge - var/buffed = FALSE - -/obj/effect/decal/cleanable/roguerune/arcyne/knowledge/invoke(list/invokers, datum/runerituals/runeritual) - runeritual = associated_ritual - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return -// if(!buffed) - var/mob/living/user = usr - user.apply_status_effect(/datum/status_effect/buff/magicknowledge) - buffed = TRUE - if(ritual_result) - pickritual.cleanup_atoms(selected_atoms) - - for(var/atom/invoker in invokers) - if(!isliving(invoker)) - continue - var/mob/living/living_invoker = invoker - if(invocation) - living_invoker.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") - if(invoke_damage) - living_invoker.apply_damage(invoke_damage, BRUTE) - to_chat(living_invoker, span_italics("[src] saps your strength!")) - do_invoke_glow() - -/obj/effect/decal/cleanable/roguerune/arcyne/leylines //used for better quality of learning, grants temporary 2 minute INT bonus. - name = "leyline attunement matrix" - desc = "geometric shapes and lines on the ground resonate with power..." - icon = 'icons/effects/96x96.dmi' - icon_state = "empowerment" - runesize = 1 - tier = 2 - pixel_x = -32 //So the big ol' 96x96 sprite shows up right - pixel_y = -32 - pixel_z = 0 - //icon_state = "6" - invocation = "Thal’miren vek’laris un’vethar!" - color = "#a70808ce" - scribe_damage = 10 - can_be_scribed = TRUE - associated_ritual = /datum/runerituals/leyattunement - var/buffed = FALSE - -/obj/effect/decal/cleanable/roguerune/arcyne/leylines/invoke(list/invokers, datum/runerituals/runeritual) - runeritual = associated_ritual - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return - var/mob/living/user = usr - if (user.mana_pool.intrinsic_recharge_sources & MANA_SOULS) //unavailable to lich - to_chat(user, span_warning("I cannot attune to leylines now.")) - else - if (user.mana_pool.intrinsic_recharge_sources & MANA_ALL_LEYLINES) - to_chat(user, span_warning("Already attuned to leylines!")) - else - user.mana_pool.set_intrinsic_recharge(MANA_ALL_LEYLINES) - playsound(user, 'sound/magic/blink.ogg', 80, FALSE) - to_chat(user, span_warning("Leylines fill me with power!")) - // SEND_SIGNAL(user, COMSIG_BAPTISM_RECEIVED, user) //for the baptism reference - - if(ritual_result) - pickritual.cleanup_atoms(selected_atoms) - - for(var/atom/invoker in invokers) - if(!isliving(invoker)) - continue - var/mob/living/living_invoker = invoker - if(invocation) - living_invoker.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") - if(invoke_damage) - living_invoker.apply_damage(invoke_damage, BRUTE) - to_chat(living_invoker, span_italics("[src] saps your strength!")) - do_invoke_glow() - -/obj/effect/decal/cleanable/roguerune/arcyne/empowerment //used for better quality of learning, grants temporary 2 minute INT bonus. - name = "empowerment array" - desc = "arcane symbols pulse upon the ground..." - icon = 'icons/effects/96x96.dmi' - icon_state = "empowerment" - tier = 2 - SET_BASE_PIXEL(-32, -32) - pixel_z = 0 - invocation = "Thal’miren vek’laris un’vethar!" - layer = SIGIL_LAYER - color = "#3A0B61" - can_be_scribed = TRUE - ritual_number = TRUE - -/obj/effect/decal/cleanable/roguerune/arcyne/empowerment/invoke(list/invokers, datum/runerituals/buff/runeritual) - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return - - var/buffedstat = runeritual.buff - for(var/mob/living/invoker in range(runesize, src)) - invoker.apply_status_effect(buffedstat) - if(ritual_result) - pickritual.cleanup_atoms(selected_atoms) - - for(var/atom/invoker in invokers) - if(!isliving(invoker)) - continue - var/mob/living/living_invoker = invoker - if(invocation) - living_invoker.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") - if(invoke_damage) - living_invoker.apply_damage(invoke_damage, BRUTE) - to_chat(living_invoker, span_italics("[src] saps your strength!")) - do_invoke_glow() - -/obj/effect/decal/cleanable/roguerune/arcyne/wall - name = "wall accession matrix" - desc = "arcane symbols litter the ground- is that a wall of some sort?" - icon_state = "wall" - tier = 2 - invocation = "Fren’aleth ar’quor!" - ritual_number = TRUE - can_be_scribed = TRUE - color = "#184075" - var/list/barriers = list() - -/obj/effect/decal/cleanable/roguerune/arcyne/wall/Destroy() - QDEL_LIST_CONTENTS(barriers) - barriers = null - return ..() - -/obj/effect/decal/cleanable/roguerune/arcyne/wall/attack_hand(mob/living/user) - if(active) - QDEL_LIST_CONTENTS(barriers) - to_chat(user, span_warning("You deactivate the [src]!")) - playsound(usr, 'sound/magic/teleport_diss.ogg', 75, TRUE) - active = FALSE - return - . = ..() - -/obj/effect/decal/cleanable/roguerune/arcyne/wall/invoke(list/invokers, datum/runerituals/runeritual) - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return - if(pickritual.tier == 1) - var/mob/living/user = usr - var/turf/target_turf = get_step(get_step(src, user.dir), user.dir) - var/turf/target_turf_two = get_step(target_turf, turn(user.dir, 90)) - var/turf/target_turf_three = get_step(target_turf, turn(user.dir, -90)) - var/turf/target_turf_four = get_step(target_turf_two, turn(user.dir, 90)) - var/turf/target_turf_five = get_step(target_turf_three, turn(user.dir, -90)) - if(!locate(/obj/structure/forcefield/casted) in target_turf) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_two) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_two, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_three) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_three, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_four) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_four, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_five) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_five, user) - src.barriers += newbarrier - active = TRUE - else - var/mob/living/user = usr - var/turf/target_turf = get_step(get_step(src, user.dir), user.dir) - var/turf/target_turf_two = get_step(target_turf, turn(user.dir, 90)) - var/turf/target_turf_three = get_step(target_turf, turn(user.dir, -90)) - var/turf/target_turf_four = get_step(target_turf_two, turn(user.dir, 90)) - var/turf/target_turf_five = get_step(target_turf_three, turn(user.dir, -90)) - var/turf/target_turfline2 = get_step(target_turf, user.dir) - var/turf/target_turfline2_two = get_step(target_turfline2, turn(user.dir, 90)) - var/turf/target_turfline2_three = get_step(target_turfline2, turn(user.dir, -90)) - var/turf/target_turfline2_four = get_step(target_turfline2_two, turn(user.dir, 90)) - var/turf/target_turfline2_five = get_step(target_turfline2_three, turn(user.dir, -90)) - if(!locate(/obj/structure/forcefield/casted) in target_turf) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_two) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_two, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_three) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_three, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_four) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_four, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turf_five) - var/obj/structure/forcefield/casted/newbarrier = new(target_turf_five, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turfline2) - var/obj/structure/forcefield/casted/newbarrier = new(target_turfline2, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turfline2_two) - var/obj/structure/forcefield/casted/newbarrier = new(target_turfline2_two, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turfline2_three) - var/obj/structure/forcefield/casted/newbarrier = new(target_turfline2_three, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turfline2_four) - var/obj/structure/forcefield/casted/newbarrier = new(target_turfline2_four, user) - src.barriers += newbarrier - if(!locate(/obj/structure/forcefield/casted) in target_turfline2_five) - var/obj/structure/forcefield/casted/newbarrier = new(target_turfline2_five, user) - src.barriers += newbarrier - active = TRUE - - if(ritual_result) - pickritual.cleanup_atoms(selected_atoms) - - for(var/atom/invoker in invokers) - if(!isliving(invoker)) - continue - var/mob/living/living_invoker = invoker - if(invocation) - living_invoker.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") - if(invoke_damage) - living_invoker.apply_damage(invoke_damage, BRUTE) - to_chat(living_invoker, span_italics("[src] saps your strength!")) - do_invoke_glow() - -/obj/effect/decal/cleanable/roguerune/arcyne/wallgreater - name = "fortress accession matrix" - desc = "A massive sigil- is that a wall in the center?" - icon = 'icons/effects/160x160.dmi' - icon_state = "wall" - tier = 3 - invocation = "Thar’morak dul’vorr keth’alor!" - ritual_number = FALSE - runesize = 2 - SET_BASE_PIXEL(-64, -64) - pixel_z = 0 - can_be_scribed = TRUE -// var/id = "arcyne_fortress" - var/datum/map_template/template - var/fortress = /datum/map_template/arcyne_fortress - var/list/barriers = list() - associated_ritual = /datum/runerituals/wall/t3 - - -/obj/effect/decal/cleanable/roguerune/arcyne/wallgreater/proc/get_template(/datum/map_template/arcyne_fortress/fortress) - - to_chat(usr, span_hierophant_warning("template retrieving")) - var/datum/map_template/temporary = new fortress - template = SSmapping.map_templates[temporary.id] - if(!template) - WARNING("Shelter template ([template.id]) not found!") - qdel(src) - - -/obj/effect/decal/cleanable/roguerune/arcyne/wallgreater/invoke(list/invokers, datum/runerituals/runeritual) - runeritual = associated_ritual - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return - if(QDELETED(src)) - return - var/turf/deploy_location = get_turf(src) - get_template(template) - - template.load(deploy_location, centered = TRUE) - to_chat(usr, span_hierophant_warning("template.load complete")) - if(ritual_result) - pickritual.cleanup_atoms(selected_atoms) - - for(var/atom/invoker in invokers) - if(!isliving(invoker)) - continue - var/mob/living/living_invoker = invoker - if(invocation) - living_invoker.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") - if(invoke_damage) - living_invoker.apply_damage(invoke_damage, BRUTE) - to_chat(living_invoker, span_italics("[src] saps your strength!")) - do_invoke_glow() - -/obj/effect/decal/cleanable/roguerune/arcyne/enchantment - invocation = "Vey’thralis en’kael dun’vora!" - -/obj/effect/decal/cleanable/roguerune/arcyne/teleport - name = "planar convergence matrix" - desc = "A large spiraling sigil that seems to thrum with power." - icon = 'icons/effects/160x160.dmi' - icon_state = "portal" - tier = 2 - req_invokers = 2 - invocation = "Xel’tharr un’korel!" - ritual_number = FALSE - req_keyword = TRUE - runesize = 2 - SET_BASE_PIXEL(-64, -64) - pixel_z = 0 - can_be_scribed = TRUE - associated_ritual = /datum/runerituals/teleport - var/listkey - -/obj/effect/decal/cleanable/roguerune/arcyne/teleport/Initialize(mapload, set_keyword) - . = ..() - var/area/A = get_area(src) - var/locname = initial(A.name) - listkey = set_keyword ? "[set_keyword] [locname]":"[locname]" - LAZYADD(GLOB.teleport_runes, src) - -/obj/effect/decal/cleanable/roguerune/arcyne/teleport/Destroy() - LAZYREMOVE(GLOB.teleport_runes, src) - return ..() - -/obj/effect/decal/cleanable/roguerune/arcyne/teleport/invoke(list/invokers, datum/runerituals/runeritual) - runeritual = associated_ritual - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return - var/mob/living/user = invokers[1] //the first invoker is always the user - var/list/potential_runes = list() - var/list/teleportnames = list() - for(var/obj/effect/decal/cleanable/roguerune/arcyne/teleport/teleport_rune as anything in GLOB.teleport_runes) - if(teleport_rune != src) - potential_runes[avoid_assoc_duplicate_keys(teleport_rune.listkey, teleportnames)] = teleport_rune - - if(!length(potential_runes)) - to_chat(user, span_warning("There are no valid runes to teleport to!")) - log_game("Teleport rune activated by [user] at [COORD(src)] failed - no other teleport runes.") - fail_invoke() - return - - var/input_rune_key = input(user, "Rune to teleport to", "Teleportation Target") as null|anything in potential_runes //we know what key they picked - if(isnull(input_rune_key)) - return - if(isnull(potential_runes[input_rune_key])) - fail_invoke() - return - var/obj/effect/decal/cleanable/roguerune/arcyne/teleport/actual_selected_rune = potential_runes[input_rune_key] //what rune does that key correspond to? - if(!Adjacent(user) || QDELETED(src) || !actual_selected_rune) - fail_invoke() - return - - var/turf/target = get_turf(actual_selected_rune) - if(target.is_blocked_turf(TRUE)) - to_chat(user, span_warning("The target rune is blocked. Attempting to teleport to it would be massively unwise.")) - log_game("Teleport rune activated by [user] at [COORD(src)] failed - destination blocked.") - fail_invoke() - return - var/movedsomething = FALSE - var/moveuserlater = FALSE - var/movesuccess = FALSE - if(ritual_result) - pickritual.cleanup_atoms(selected_atoms) - for(var/atom/movable/A in range(runesize, src)) - if(istype(A, /obj/effect/dummy/phased_mob)) - continue - if(ismob(A)) - if(!isliving(A)) //Let's not teleport ghosts and AI eyes. - continue - if(A == user) - moveuserlater = TRUE - movedsomething = TRUE - continue - if(!A.anchored) - movedsomething = TRUE - if(do_teleport(A, target, channel = TELEPORT_CHANNEL_CULT)) - movesuccess = TRUE - if(movedsomething) - //..() - playsound(src, 'sound/magic/cosmic_expansion.ogg', 50, TRUE) - playsound(target, 'sound/magic/cosmic_expansion.ogg', 50, TRUE) - if(moveuserlater) - if(do_teleport(user, target, channel = TELEPORT_CHANNEL_CULT)) - movesuccess = TRUE - if(movesuccess) - visible_message(span_warning("There is a sharp crack of inrushing air, and everything above the rune disappears!"), null, "You hear a sharp crack.") - to_chat(user, span_cult("You[moveuserlater ? "r vision blurs, and with a falling feeling you suddenly appear somewhere else":" send everything above the rune away"].")) - else - to_chat(user, span_cult("You[moveuserlater ? "r vision blurs briefly, but nothing happens":" try send everything above the rune away, but the teleportation fails"].")) - if(movesuccess) - target.visible_message(span_warning("There is a boom of outrushing air as something appears above the rune!"), null, "You hear a boom.") - for(var/atom/invoker in invokers) - if(!isliving(invoker)) - continue - var/mob/living/living_invoker = invoker - if(invocation) - living_invoker.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") - if(invoke_damage) - living_invoker.apply_damage(invoke_damage, BRUTE) - to_chat(living_invoker, span_italics("[src] saps your strength!")) - else - fail_invoke() - -/obj/effect/decal/cleanable/roguerune/arcyne/summoning //32x32 rune t1(one tile) - name = "confinement matrix" - desc = "A relatively basic confinement matrix used to hold small things when summoned." - ritual_number = TRUE - invocation = "Rhegal vex'ultraa!" - tier = 1 - can_be_scribed = TRUE - var/summoning = FALSE - var/mob/living/simple_animal/summoned_mob - -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/Destroy() - if(summoning) - REMOVE_TRAIT(summoned_mob, TRAIT_PACIFISM, MAGIC_TRAIT) //can't kill while planar bound. - summoned_mob.status_flags -= GODMODE//remove godmode - summoned_mob.candodge = TRUE - summoned_mob.binded = FALSE - summoned_mob.SetParalyzed(0) - summoned_mob = null - summoning = FALSE - .=..() - -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/attack_hand(mob/living/user) - if(summoning && isarcyne(user)) - to_chat(user, span_warning("You release the summon from its containment!")) - playsound(usr, 'sound/magic/teleport_diss.ogg', 75, TRUE) - do_invoke_glow() - sleep(20) - animate(summoned_mob, color = null,time = 5) - REMOVE_TRAIT(summoned_mob, TRAIT_PACIFISM, MAGIC_TRAIT) //can't kill while planar bound. - summoned_mob.status_flags -= GODMODE//remove godmode - summoned_mob.candodge = TRUE - summoned_mob.binded = FALSE - summoned_mob.SetParalyzed(0) - summoned_mob = null - summoning = FALSE - return - . = ..() - -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/invoke(list/invokers, datum/runerituals/runeritual) - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return - // All the components have been invisibled, time to actually do the ritual. Call on_finished_recipe - // (Note: on_finished_recipe may sleep in the case of some rituals like summons, which expect ghost candidates.) - // - If the ritual was success (Returned TRUE), proceede to clean up the atoms involved in the ritual. The result has already been spawned by this point. - // - If the ritual failed for some reason (Returned FALSE), likely due to no ghosts taking a role or an error, we shouldn't clean up anything, and reset. - if(ismob(ritual_result)) - summoned_mob = ritual_result - src.summoning = TRUE - if(ritual_result) - pickritual.cleanup_atoms(selected_atoms) - - for(var/atom/invoker in invokers) - if(!isliving(invoker)) - continue - var/mob/living/living_invoker = invoker - if(invocation) - living_invoker.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") - if(invoke_damage) - living_invoker.apply_damage(invoke_damage, BRUTE) - to_chat(living_invoker, span_italics("[src] saps your strength!")) - do_invoke_glow() - -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/mid// 96x96 rune t2(3x3 tile) - name = "sealate confinement matrix" - desc = "An adept confinement matrix improved with the addition of a sealate matrix; used to hold things when summoned." - icon = 'icons/effects/96x96.dmi' - icon_state = "sealate" - runesize = 1 - tier = 2 - SET_BASE_PIXEL(-32, -32) - pixel_z = 0 - can_be_scribed = TRUE - -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/adv //160x160 rune t2(5x5 tile) - name = "warded sealate confinement matrix" - desc = "A thoroughly-warded confinement matrix improved with the addition of a sealate matrix; used to hold larger, dangerous things when summoned." - icon = 'icons/effects/160x160.dmi' - icon_state = "warded" - runesize = 2 - tier = 3 - SET_BASE_PIXEL(-64, -64) - pixel_z = 0 - can_be_scribed = TRUE - -/obj/effect/decal/cleanable/roguerune/arcyne/summoning/max //224x224 rune t3(7x7 tile) - name = "noc's eye warded sealate confinement matrix" - desc = "A thoroughly-warded confinement matrix improved with a Noc's eye sealing measure and the addition of a sealate matrix; used to hold the largest, most dangerous things summonable." - icon = 'icons/effects/224x224.dmi' - icon_state = "huge_runeblued" - runesize = 3 - req_invokers = 3 - tier = 4 - SET_BASE_PIXEL(-96, -96) - pixel_z = 0 - can_be_scribed = TRUE - -// /obj/effect/decal/cleanable/roguerune/arcyne/enchanting ?? - -/obj/effect/decal/cleanable/roguerune/divine //To be used for divine rituals. - magictype = "divine" - can_be_scribed = FALSE - -/obj/effect/decal/cleanable/roguerune/divine/attack_hand(mob/living/user) - if(!isdivine(user)) - to_chat(user, span_warning("You aren't able to understand the words of [src].")) - return - . = ..() - -/obj/effect/decal/cleanable/roguerune/druid //to be used with druid magick - magictype = "druid" - can_be_scribed = FALSE - -/obj/effect/decal/cleanable/roguerune/druid/attack_hand(mob/living/user) - if(!isdruid(user)) - to_chat(user, span_warning("You aren't able to understand the words of [src].")) - return - . = ..() - -/obj/effect/decal/cleanable/roguerune/blood //to be used with blood magick - magictype = "blood" - can_be_scribed = FALSE - -/obj/effect/decal/cleanable/roguerune/blood/attack_hand(mob/living/user) - if(!isblood(user)) - to_chat(user, span_warning("You aren't able to understand the words of [src].")) - return - . = ..() - -/obj/effect/decal/cleanable/roguerune/arcyne/attunement - name = "arcyne attunement matrix" - desc = "A large matrix designed to imbue the energies of materials." - icon = 'icons/effects/160x160.dmi' - icon_state = "imbuement" - tier = 2 - req_invokers = 2 - invocation = "Xel’thix un’oral!" - ritual_number = FALSE - req_keyword = TRUE - runesize = 2 - SET_BASE_PIXEL(-64, -64) - pixel_z = 0 - can_be_scribed = TRUE - associated_ritual = /datum/runerituals/attunement - takes_all_items = TRUE - -/obj/effect/decal/cleanable/roguerune/arcyne/attunement/invoke(list/invokers, datum/runerituals/runeritual) - runeritual = associated_ritual - if(!..()) //VERY important. Calls parent and checks if it fails. parent/invoke has all the checks for ingredients - return - var/mob/living/user = invokers[1] //the first invoker is always the user - var/datum/runerituals/attunement/attune_ritual = pickritual - - var/list/attunements = attune_ritual.attunement_modifiers - - for(var/datum/attunement/attunement as anything in attunements) - user.mana_pool?.adjust_attunement(attunement, attunements[attunement]) - - pickritual.cleanup_atoms(selected_atoms) - do_invoke_glow() diff --git a/code/datums/rituals/runes/_base.dm b/code/datums/rituals/runes/_base.dm new file mode 100644 index 00000000000..f75911f0919 --- /dev/null +++ b/code/datums/rituals/runes/_base.dm @@ -0,0 +1,255 @@ + +/obj/effect/decal/cleanable/ritual_rune + name = "ritual rune" + desc = "Strange symbols pulse upon the ground..." + anchored = TRUE + icon = 'icons/obj/rune.dmi' + icon_state = "6" + resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF + obj_flags = NONE + layer = SIGIL_LAYER + color = null + + ///controls who can invoke and which skill check is used + var/datum/attribute/skill/magic/magictype = /datum/attribute/skill/magic/arcane + /// Tile radius used when scanning for co-invokers and nearby atoms. 0 = the rune's own tile only. + var/runesize = 0 + var/invoker_name = "basic rune" + /// Shown to players with the appropriate magic skill when they examine the rune + var/invoker_desc = "a basic rune with no function." + /// Words spoken aloud by invokers when the rune fires + var/invocation = "Aiy ele-mayo!" + /// How many eligible invokers must be standing near the rune for it to fire + var/req_invokers = 1 + /// Optional override text for the invoker count requirement shown to players + var/req_invokers_text + /// Prevents double-activation while a ritual is resolving + var/rune_in_use = FALSE + /// If TRUE, qdel is logged with the erasing player's name + var/log_when_erased = FALSE + /// Set TRUE when this rune presents a ritual-picker menu rather than having a single fixed ritual + var/ritual_number = FALSE + /// If FALSE, this rune can only be placed by admins or special means + var/can_be_scribed = TRUE + var/erase_time = 1.5 SECONDS + var/scribe_delay = 4 SECONDS + /// If TRUE, the normal scribe-speed boost is suppressed on certain turfs + var/no_scribe_boost = FALSE + /// Flat bonus applied to spells or spellbook reading when standing on this rune + var/spellbonus = 0 + /// Brute damage dealt to the scriber on creation + var/scribe_damage = 5 + /// Brute damage dealt to each invoker when the rune fires + var/invoke_damage = 0 + /// If TRUE, the player must supply a keyword when scribing this rune + var/req_keyword = FALSE + var/keyword + /// Global proc path called while the rune is being created (optional) + var/started_creating + /// Global proc path called when rune creation is interrupted (optional) + var/failed_to_create + var/active = FALSE + /// Tier of this rune. Used to gate which rituals are available. 0 = no tiers. + var/tier = 1 + /// Return value of the last ritual's on_finished_recipe + var/ritual_result + /// Atoms within runesize of the rune, populated at invoke time + var/list/atom/movable/atoms_in_range + /// The runeritual datum instantiated for the current invocation + var/datum/runerituals/pickritual + /// Atoms selected to satisfy the ritual's ingredient requirements + var/list/selected_atoms + /// For single-ritual runes: the fixed ritual typepath. Leave null when ritual_number = TRUE. + var/associated_ritual = null + /// If TRUE, every item with attunement_values on the rune is pulled into selected_atoms + var/takes_all_items = FALSE + +/obj/effect/decal/cleanable/ritual_rune/Initialize(mapload, set_keyword) + . = ..() + if(set_keyword) + keyword = set_keyword + +/// Plays the brief flash-and-fade glow when a rune successfully fires. +/obj/effect/decal/cleanable/ritual_rune/proc/do_invoke_glow() + set waitfor = FALSE + animate(src, transform = matrix() * 2, alpha = 0, time = 5, flags = ANIMATION_END_NOW) + sleep(0.5 SECONDS) + animate(src, transform = matrix(), alpha = 255, time = 0, flags = ANIMATION_END_NOW) + +/// Called when the rune fails to activate. Shows a fizzle and resets rune_in_use. +/obj/effect/decal/cleanable/ritual_rune/proc/fail_invoke() + visible_message(span_warning("The markings pulse with a small flash of light, then fall dark.")) + var/oldcolor = color + color = rgb(255, 0, 0) + animate(src, color = oldcolor, time = 5) + addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_atom_colour)), 0.5 SECONDS) + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/attack_hand(mob/living/user) + if(!ritual_number && !associated_ritual) + return ..() // this is basically are we a type 2 rune + if(GET_MOB_SKILL_VALUE(user, magictype) < SKILL_LEVEL_NONE) + to_chat(user, span_warning("You aren't able to understand the words of [src].")) + return + + if(rune_in_use) + to_chat(user, span_notice("Someone is already using this rune.")) + return + if(.) + return + + var/list/invokers = can_invoke(user) + var/have = length(invokers) + if(have < req_invokers) + to_chat(user, span_danger("You need [req_invokers - have] more adjacent invokers to use this rune in such a manner.")) + fail_invoke() + return + + if(!ritual_number) + // Single fixed ritual - no menu needed. + if(associated_ritual) + invoke(invokers, associated_ritual) + return + + // Multi-ritual rune: build the ritual list for this rune's tier, then ask. + var/list/rituals = get_ritual_list_for_rune() + if(!length(rituals)) + fail_invoke() + return + + var/chosen_name = input(user, "Rituals", "Vanderlin") as null|anything in rituals + var/datum/runerituals/chosen = rituals[chosen_name] + if(!chosen) + rune_in_use = FALSE + return + if(chosen.tier > src.tier) + to_chat(user, span_hierophant_warning("Your ritual rune is not strong enough to perform this ritual.")) + rune_in_use = FALSE + return + + invoke(invokers, chosen) + . = ..() + +/// Returns the appropriate ritual list for this rune type and tier. +/// Override on subtype if you need a different pool. +/obj/effect/decal/cleanable/ritual_rune/proc/get_ritual_list_for_rune() + return GLOB.allowedrunerituallist + + +/// Marks the rune as in-use and returns the list of eligible invokers. +/// Always includes user as the first entry if supplied. +/obj/effect/decal/cleanable/ritual_rune/proc/can_invoke(mob/living/user = null) + rune_in_use = TRUE + var/list/invokers = list() + if(user) + invokers += user + if(req_invokers <= 1) + return invokers + for(var/mob/living/invoker in range(runesize, src)) + if(invoker == user) + continue + if(!invoker.can_speak() || invoker.stat != CONSCIOUS) + continue + if(GET_MOB_SKILL_VALUE(invoker, magictype) > SKILL_LEVEL_NONE) + invokers += invoker + return invokers + + +/obj/effect/decal/cleanable/ritual_rune/proc/invoke(list/invokers, datum/runerituals/runeritual) + SHOULD_CALL_PARENT(TRUE) + rune_in_use = FALSE + + // Gather all movable, visible, non-abstract atoms in range. + atoms_in_range = list() + for(var/atom/movable/close_atom as anything in range(runesize, src)) + if(isitem(close_atom)) + var/obj/item/close_item = close_atom + if(close_item.item_flags & ABSTRACT) + continue + if(close_atom.invisibility || close_atom == usr || close_atom == src) + continue + atoms_in_range += close_atom + + pickritual = new runeritual() + + // Copy requirement and ban lists so we can decrement without mutating the datum. + var/list/requirements = pickritual.required_atoms.Copy() + var/list/banned = pickritual.banned_atom_types.Copy() + selected_atoms = list() + + // Match nearby atoms against requirements. + for(var/atom/movable/nearby as anything in atoms_in_range) + for(var/req_type in requirements) + if(requirements[req_type] <= 0) + continue + if(islist(req_type)) + if(!is_type_in_list(nearby, req_type)) + continue + else if(!istype(nearby, req_type)) + continue + if(length(banned) && (nearby.type in banned)) + continue + selected_atoms |= nearby + requirements[req_type]-- + + // Attunement items are always collected when takes_all_items is set. + if(takes_all_items && isitem(nearby) && length(nearby:attunement_values)) + selected_atoms |= nearby + + // Report any unfulfilled requirements. + var/list/missing = list() + for(var/req_type in requirements) + if(requirements[req_type] <= 0) + continue + var/count = requirements[req_type] + var/label + if(islist(req_type)) + var/list/options = list() + for(var/possible as anything in req_type) + options += pickritual.parse_required_item(possible) + label = "[count] [english_list(options, and_text = "or")]" + else + label = pickritual.parse_required_item(req_type) + missing += label + + if(length(missing)) + to_chat(usr, span_hierophant_warning("Ritual failed, missing components!")) + to_chat(usr, span_hierophant_warning("You are missing [english_list(missing)] in order to complete the ritual \"[pickritual.name]\".")) + fail_invoke() + return FALSE + + playsound(usr, 'sound/magic/teleport_diss.ogg', 75, TRUE) + ritual_result = pickritual.on_finished_recipe(usr, selected_atoms, loc) + return TRUE + +/// Plays invocations, deals damage, and fires the glow after a successful invoke. +/// Call this at the end of every subtype invoke() proc. +/obj/effect/decal/cleanable/ritual_rune/proc/finish_invoke(list/invokers) + for(var/atom/invoker_atom in invokers) + if(!isliving(invoker_atom)) + continue + var/mob/living/M = invoker_atom + if(invocation) + M.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") + if(invoke_damage) + M.apply_damage(invoke_damage, BRUTE) + to_chat(M, span_italics("[src] saps your strength!")) + do_invoke_glow() + +/obj/effect/decal/cleanable/ritual_rune/arcyne + name = "arcane ritual rune" + desc = "Subtype used for arcane rituals — you should not be seeing this." + magictype = /datum/attribute/skill/magic/arcane + can_be_scribed = FALSE + +/obj/effect/decal/cleanable/ritual_rune/divine + magictype = /datum/attribute/skill/magic/holy + can_be_scribed = FALSE + +/obj/effect/decal/cleanable/ritual_rune/druid + magictype = /datum/attribute/skill/magic/druidic + can_be_scribed = FALSE + +/obj/effect/decal/cleanable/ritual_rune/blood + magictype = /datum/attribute/skill/magic/blood + can_be_scribed = FALSE diff --git a/code/datums/rituals/runes/arcane/attunement.dm b/code/datums/rituals/runes/arcane/attunement.dm new file mode 100644 index 00000000000..cea57b357a5 --- /dev/null +++ b/code/datums/rituals/runes/arcane/attunement.dm @@ -0,0 +1,46 @@ + +/obj/effect/decal/cleanable/ritual_rune/arcyne/attunement + name = "arcyne attunement matrix" + desc = "A large matrix designed to imbue the energies of materials." + icon = 'icons/effects/160x160.dmi' + icon_state = "imbuement" + tier = 2 + req_invokers = 2 + invocation = "Xel'thix un'oral!" + req_keyword = TRUE + runesize = 2 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + can_be_scribed = TRUE + associated_ritual = /datum/runerituals/attunement + takes_all_items = TRUE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/attunement/invoke(list/invokers, datum/runerituals/runeritual) + runeritual = associated_ritual + if(!..()) + return + var/mob/living/user = invokers[1] + var/datum/runerituals/attunement/attune = pickritual + for(var/datum/attunement/att as anything in attune.attunement_modifiers) + user.mana_pool?.adjust_attunement(att, attune.attunement_modifiers[att]) + pickritual.cleanup_atoms(selected_atoms) + do_invoke_glow() + +/datum/runerituals/attunement + name = "arcyne attunement" + blacklisted = FALSE + required_atoms = list( + /obj/item/reagent_containers/food/snacks/produce/manabloom = 1, + /obj/item/natural/melded/t1 = 1 + ) + /// Assoc list of [/datum/attunement] = modifier value, accumulated from attuned items + var/list/attunement_modifiers = list() + +/datum/runerituals/attunement/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + for(var/obj/item/item in selected_atoms) + if(!length(item.attunement_values)) + continue + for(var/datum/attunement/att in item.attunement_values) + attunement_modifiers |= att + attunement_modifiers[att] += item.attunement_values[att] + return TRUE diff --git a/code/datums/rituals/runes/arcane/crafting.dm b/code/datums/rituals/runes/arcane/crafting.dm new file mode 100644 index 00000000000..06c9ef498e0 --- /dev/null +++ b/code/datums/rituals/runes/arcane/crafting.dm @@ -0,0 +1,248 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting + name = "arcyne crafting matrix" + desc = "A large, intricate sigil that seems to hunger for materials..." + icon = 'icons/effects/160x160.dmi' + icon_state = "imbuement" + tier = 2 + runesize = 2 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + invocation = "Veth'kael un'dorath shar!" + can_be_scribed = TRUE + color = "#7B2FBE" + + /// Ordered list of /datum/crafting_slot each holds position + placed item ref + var/list/datum/crafting_slot/slots = list() + /// The matched recipe for the current staging, null if no match yet + var/datum/arcyne_crafting_recipe/matched_recipe = null + /// TRUE while the animation is playing; blocks all interaction + var/animating = FALSE + +/// Returns a list of [x, y] pixel offset pairs for N ingredients. +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/get_slot_layout(count) + switch(count) + if(1) + return list(list(0, 0)) + if(2) + return list(list(-44, 0), list(44, 0)) + if(3) + return list(list(0, 48), list(-44, -28), list(44, -28)) + if(4) + return list(list(-44, 44), list(44, 44), list(-44, -44), list(44, -44)) + if(5) + return list(list(0, 56), list(27, 18), list(34, -48), list(-34, -48), list(-27, 18)) + if(6) + return list(list(0, 56), list(48, 28), list(48, -28), list(0, -56), list(-48, -28), list(-48, 28)) + //evenly space around a circle at radius 48px if we are above 6 items + var/list/offsets = list() + for(var/i = 0, i < count, i++) + var/angle = (360 / count) * i - 90 + offsets += list(list(round(48 * cos(angle)), round(48 * sin(angle)))) + return offsets + +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/attack_hand(mob/living/user) + if(animating) + to_chat(user, span_notice("The rune is already working...")) + return + + //try to invoke if a recipe is matched, or eject if staged. + if(!user.get_active_held_item()) + if(matched_recipe) + try_invoke(user) + else if(length(slots)) + to_chat(user, span_hierophant_warning("No recipe matches what's been placed here.")) + else + . = ..() + return + +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/attackby(obj/item/W, mob/user, list/modifiers) + if(animating) + to_chat(user, span_notice("The rune is already working...")) + return + // try to stage it. + if(!try_place_item(user, W)) + return ..() + +/// Attempts to place the held item into the next open slot. +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/try_place_item(mob/living/user, obj/item/item) + // Check if the item is already staged on this rune. + for(var/datum/crafting_slot/S in slots) + if(S.item == item) + to_chat(user, span_notice("That's already placed on the rune.")) + return + + // Find the maximum ingredient count across all recipes to cap slot count. + var/static/max_slots = 0 + if(!max_slots) + for(var/datum/arcyne_crafting_recipe/recipe_type as anything in subtypesof(/datum/arcyne_crafting_recipe)) + var/datum/arcyne_crafting_recipe/R = new recipe_type + max_slots = max(max_slots, length(R.ingredients)) + qdel(R) + + if(length(slots) >= max_slots) + to_chat(user, span_hierophant_warning("The rune can't hold any more ingredients.")) + return + + // Drop the item from the user's hand onto the rune's turf, then stage it. + user.temporarilyRemoveItemFromInventory(item) + item.forceMove(get_turf(src)) + + var/datum/crafting_slot/slot = new() + slot.item = item + slots += slot + + // Assign pixel offsets once we know how many slots we'll need. + // We re-layout all existing slots each time so they stay symmetric. + relayout_slots() + + for(var/datum/crafting_slot/S in slots) + if(S.item) + S.item.anchored = TRUE + slide_item_to_slot(S.item, S) + + to_chat(user, span_cultsmall("The item settles into place on the rune...")) + playsound(src, 'sound/magic/glass.ogg', 60, TRUE) + + // Check if the current staged items match any recipe. + matched_recipe = find_matching_recipe() + if(matched_recipe) + to_chat(user, span_hierophant_warning("The rune hums with readiness. Invoke it when ready.")) + return TRUE + +/// Recalculates pixel positions for all staged slots based on current count. +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/relayout_slots() + var/list/offsets = get_slot_layout(length(slots)) + for(var/i = 1, i <= length(slots), i++) + var/datum/crafting_slot/S = slots[i] + S.px = offsets[i][1] + S.py = offsets[i][2] + +/// Animates a single item sliding from its current pixel position to its slot. +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/slide_item_to_slot(obj/item/item, datum/crafting_slot/slot) + animate(item, pixel_x = slot.px, pixel_y = slot.py, time = 0.7 SECONDS, flags = ANIMATION_END_NOW) + +/// Scans all recipe subtypes and returns the first whose ingredients are fully satisfied. +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/find_matching_recipe() + var/list/staged_types = list() + for(var/datum/crafting_slot/S in slots) + staged_types += S.item.type + + for(var/recipe_type as anything in subtypesof(/datum/arcyne_crafting_recipe)) + var/datum/arcyne_crafting_recipe/R = new recipe_type + var/list/needed = R.ingredients.Copy() + if(length(needed) != length(slots)) + continue + // Try to match each staged item against the needed list. + var/list/remaining = needed.Copy() + var/matched = TRUE + for(var/datum/crafting_slot/S in slots) + var/found = FALSE + for(var/req_type in remaining) + if(istype(S.item, req_type)) + remaining.Remove(req_type) + found = TRUE + break + if(!found) + matched = FALSE + break + if(matched && !length(remaining)) + return new recipe_type() + qdel(R) + return null + +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/try_invoke(mob/living/user) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(user, span_warning("You aren't able to invoke these symbols.")) + return + if(rune_in_use) + to_chat(user, span_notice("The rune is already active.")) + return + rune_in_use = TRUE + animating = TRUE + + // Check skill gate. + var/skill_level = GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) + if(skill_level < matched_recipe.required_skill) + to_chat(user, span_hierophant_warning("My arcyne is not refined enough to complete this working...")) + abort_ritual() + return + + user.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") + playsound(src, 'sound/magic/cosmic_expansion.ogg', 60, TRUE) + SpinAnimation( + speed = 1.5 SECONDS, + loops = 1, + clockwise = TRUE, + segments = 6, // smoother than default 3 + parallel = TRUE + ) + + INVOKE_ASYNC(src, PROC_REF(run_craft_animation), user) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/run_craft_animation(mob/living/user) + var/turf/center = get_turf(src) + + //slide all items to center and fade them out. + for(var/datum/crafting_slot/S in slots) + animate(S.item, + pixel_x = 0, pixel_y = 0, + alpha = 0, + transform = matrix() * 0.2, + time = 1.5 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(1.5 SECONDS) + + //delete ingredients. + for(var/datum/crafting_slot/S in slots) + qdel(S.item) + slots.Cut() + + sleep(0.2 SECONDS) + + //spawn output invisible and floating, then fade it in. + var/obj/item/result = new matched_recipe.output(center) + result.alpha = 0 + var/saved_transform = result.transform + result.transform = matrix() * 0.1 + animate(result, + alpha = 255, + transform = saved_transform, + pixel_y = 8, + time = 1.2 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(1.2 SECONDS) + animate(result, pixel_y = 0, time = 0.3 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.3 SECONDS) + + playsound(src, 'sound/magic/blink.ogg', 80, TRUE) + + matched_recipe = null + animating = FALSE + rune_in_use = FALSE + do_invoke_glow() + +/// Releases all staged items and resets state without crafting anything. +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/proc/abort_ritual() + for(var/datum/crafting_slot/S in slots) + if(S.item && !QDELETED(S.item)) + S.item.anchored = FALSE + animate(S.item, pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + slots.Cut() + matched_recipe = null + animating = FALSE + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/crafting/Destroy() + abort_ritual() + return ..() + +///this is really just an ease thing can be removed if we really care +/datum/crafting_slot + /// The item currently occupying this slot + var/obj/item/item = null + /// Pixel X offset from the rune's tile center + var/px = 0 + /// Pixel Y offset from the rune's tile center + var/py = 0 diff --git a/code/datums/rituals/runes/arcane/deconstruction.dm b/code/datums/rituals/runes/arcane/deconstruction.dm new file mode 100644 index 00000000000..2588c43622f --- /dev/null +++ b/code/datums/rituals/runes/arcane/deconstruction.dm @@ -0,0 +1,249 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting + name = "arcyne decrafting matrix" + desc = "A jagged, fractured sigil that seems to want to take things apart..." + icon = 'icons/effects/160x160.dmi' + icon_state = "imbuement" // reuse the same base sprite; tint differentiates it + tier = 2 + runesize = 2 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + invocation = "Keth'daal un'shar veth!" + can_be_scribed = TRUE + color = "#BE2F7B" // magenta-pink to distinguish from the purple crafter + + /// The single staged item, null when empty + var/obj/item/staged_item = null + /// The matched recipe datum (either arcyne or repeatable), null if none found + var/datum/decraft_match/matched = null + /// TRUE while the break-apart animation is playing; blocks all interaction + var/animating = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/attack_hand(mob/living/user) + if(animating) + to_chat(user, span_notice("The rune is already working...")) + return + + if(!user.get_active_held_item()) + if(staged_item) + if(matched) + try_invoke(user) + else + to_chat(user, span_hierophant_warning("The rune does not know how to unmake what has been placed here.")) + else + return ..() + return + +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/attackby(obj/item/W, mob/user, list/modifiers) + if(animating) + to_chat(user, span_notice("The rune is already working...")) + return + if(!try_stage_item(user, W)) + return ..() + +/// Accepts exactly one item. Replaces any previously staged item (returning it +/// to the ground) so the player can swap without having to erase and re-draw. +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/proc/try_stage_item(mob/living/user, obj/item/item) + if(staged_item == item) + to_chat(user, span_notice("That's already placed on the rune.")) + return TRUE // handled, don't fall through + + // Eject whatever was there before + if(staged_item && !QDELETED(staged_item)) + staged_item.anchored = FALSE + animate(staged_item, pixel_x = 0, pixel_y = 0, time = 0.3 SECONDS, flags = ANIMATION_END_NOW) + + user.temporarilyRemoveItemFromInventory(item) + item.forceMove(get_turf(src)) + item.anchored = TRUE + animate(item, pixel_x = 0, pixel_y = 0, time = 0.7 SECONDS, flags = ANIMATION_END_NOW) + + staged_item = item + matched = find_matching_recipe(item) + + if(matched) + to_chat(user, span_hierophant_warning("The rune trembles with unmaking. Invoke it when ready.")) + else + to_chat(user, span_warning("The rune does not recognise this item it cannot be unravelled here.")) + + playsound(src, 'sound/magic/glass.ogg', 60, TRUE) + return TRUE + +/* + * /datum/decraft_match + * Lightweight holder so we don't keep the full recipe datum alive between + * staging and invocation, and so we have a unified interface regardless of + * where the recipe came from. + * + * ingredient_types – list of typepaths to spawn (repeatable) + * OR an assoc list of type = count (arcyne) + * required_skill – minimum arcane skill level needed + * recipe_name – human-readable name for messages + */ +/datum/decraft_match + var/recipe_name = "unknown recipe" + var/list/ingredients = list() // flat list of typepaths to spawn + var/required_skill = SKILL_LEVEL_NONE + +/datum/decraft_match/New(name, list/ingredient_list, skill) + recipe_name = name + ingredients = ingredient_list + required_skill = skill + +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/proc/find_matching_recipe(obj/item/item) + for(var/recipe_type as anything in subtypesof(/datum/arcyne_crafting_recipe)) + var/datum/arcyne_crafting_recipe/R = new recipe_type + if(!ispath(R.output) || !istype(item, R.output)) + qdel(R) + continue + // Build a flat ingredient list respecting counts + var/list/flat = list() + for(var/ing_type in R.ingredients) + flat += ing_type // arcyne ingredients list is already a flat list of types + var/datum/decraft_match/M = new(recipe_type, flat, R.required_skill) + qdel(R) + return M + + for(var/recipe_type as anything in subtypesof(/datum/repeatable_crafting_recipe)) + var/datum/repeatable_crafting_recipe/R = new recipe_type + if(!ispath(R.output) || !istype(item, R.output)) + qdel(R) + continue + var/list/flat = list() + // requirements is an assoc list of type = count + for(var/req_type in R.requirements) + var/count = R.requirements[req_type] + for(var/i = 1, i <= count, i++) + flat += req_type + var/datum/decraft_match/M = new(recipe_type, flat, initial(R.minimum_skill_level)) + qdel(R) + return M + + return null + +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/proc/try_invoke(mob/living/user) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(user, span_warning("You aren't able to invoke these symbols.")) + return + if(rune_in_use) + to_chat(user, span_notice("The rune is already active.")) + return + rune_in_use = TRUE + animating = TRUE + + var/skill_level = GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) + if(skill_level < matched.required_skill) + to_chat(user, span_hierophant_warning("My arcyne is not refined enough to perform this unmaking...")) + abort_ritual() + return + + if(!isnull(user.mana_pool) && user.mana_pool.amount > 20) + var/drain = min(user.mana_pool.amount, 20) + user.mana_pool.adjust_mana(-drain) + else + to_chat(user, span_notice("You lack the mana to activate this.")) + return + + user.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") + playsound(src, 'sound/magic/cosmic_expansion.ogg', 60, TRUE) + SpinAnimation( + speed = 1.5 SECONDS, + loops = 1, + clockwise = FALSE, // spins counter-clockwise to feel like "undoing" + segments = 6, + parallel = TRUE + ) + + INVOKE_ASYNC(src, PROC_REF(run_decraft_animation), user) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/proc/run_decraft_animation(mob/living/user) + var/turf/center = get_turf(src) + + animate(staged_item, pixel_x = 6, time = 0.15 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.15 SECONDS) + animate(staged_item, pixel_x = -6, time = 0.15 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.15 SECONDS) + animate(staged_item, pixel_x = 4, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + animate(staged_item, pixel_x = 0, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + + animate(staged_item, + alpha = 0, + transform = matrix() * 1.6, + time = 0.8 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(0.8 SECONDS) + + qdel(staged_item) + staged_item = null + + sleep(0.15 SECONDS) + + var/list/ingredient_types = matched.ingredients + var/count = length(ingredient_types) + var/list/offsets = get_spawn_offsets(count) + + var/list/spawned = list() + for(var/i = 1, i <= count, i++) + var/ing_type = ingredient_types[i] + var/obj/item/ing = new ing_type(center) + ing.alpha = 0 + ing.transform = matrix() * 0.1 + ing.pixel_x = 0 + ing.pixel_y = 0 + spawned += ing + + sleep(0.1 SECONDS) + + for(var/i = 1, i <= length(spawned), i++) + var/obj/item/ing = spawned[i] + animate(ing, + alpha = 255, + transform = matrix(), + pixel_x = offsets[i][1], + pixel_y = offsets[i][2], + time = 1.0 SECONDS, + flags = ANIMATION_END_NOW + ) + + sleep(1.0 SECONDS) + + // Let them drop naturally (unanchor so they sit on the tile) + for(var/obj/item/ing in spawned) + animate(ing, pixel_x = 0, pixel_y = 0, time = 0.4 SECONDS, flags = ANIMATION_END_NOW) + + sleep(0.4 SECONDS) + + playsound(src, 'sound/magic/blink.ogg', 80, TRUE) + + matched = null + animating = FALSE + rune_in_use = FALSE + do_invoke_glow() + +/// Returns a list of [x, y] pixel offset pairs for N spawned ingredients, +/// arranged in a ring so they fan outward from the rune centre. +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/proc/get_spawn_offsets(count) + if(count <= 0) + return list() + var/list/offsets = list() + var/radius = clamp(count * 10, 36, 64) + for(var/i = 0, i < count, i++) + var/angle = (360 / count) * i - 90 + offsets += list(list(round(radius * cos(angle)), round(radius * sin(angle)))) + return offsets + +/// Releases the staged item and resets all state without decrafting anything. +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/proc/abort_ritual() + if(staged_item && !QDELETED(staged_item)) + staged_item.anchored = FALSE + animate(staged_item, pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + staged_item = null + matched = null + animating = FALSE + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/decrafting/Destroy() + abort_ritual() + return ..() diff --git a/code/datums/rituals/runes/arcane/draining/_base.dm b/code/datums/rituals/runes/arcane/draining/_base.dm new file mode 100644 index 00000000000..2492dfae3d1 --- /dev/null +++ b/code/datums/rituals/runes/arcane/draining/_base.dm @@ -0,0 +1,227 @@ +/// Tiles (3D-distance) within which subscribers receive the buff refresh +#define MANA_SIPHON_BUFF_RANGE 7 +/// Mana drained from anything per process tick +#define MANA_SIPHON_BASE_DRAIN 0.4 +/// Process rate in deciseconds +#define MANA_SIPHON_PROCESS_RATE (2 SECONDS) +/// Duration granted/refreshed to the buff status effect (deciseconds) +#define MANA_SIPHON_BUFF_DURATION (3 SECONDS) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon + name = "arcyne mana siphon" + desc = "A hungry, pulsing sigil that draws power from the world and feeds it to those bound to it..." + icon = 'icons/effects/160x160.dmi' + icon_state = "imbuement" + tier = 2 + runesize = 3 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + invocation = "Shar'kael un'veth draum!" + can_be_scribed = TRUE + color = "#2FBEBE" + + var/datum/status_effect/mana_siphon_buff/buff = /datum/status_effect/mana_siphon_buff + /// Whether the siphon is currently draining and buffing + var/siphon_active = FALSE + /// The mob who activated the rune (fallback mana source, also sets the "anchor" for range checks) + var/mob/living/activating_mob = null + /// List of mobs who have paid the blood price to receive the buff + var/list/mob/living/subscribers = list() + /// Internal process timer handle + var/process_timer = null + ///how fast we loop + var/loop_speed = MANA_SIPHON_PROCESS_RATE + ///our mana cost per loop + var/mana_cost = MANA_SIPHON_BASE_DRAIN + ///are we a subscribable rune? some aren't like crop_growth + var/user_facing = TRUE + ///anti chat spam basically + COOLDOWN_DECLARE(drain_message) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/proc/set_active_visuals(active) + if(active) + // Pulse the rune brighter when siphoning + animate(src, alpha = 255, time = 0.3 SECONDS, flags = ANIMATION_END_NOW) + set_light(1.6, 1.6, 1.0, l_color = "#2FBEBE") + else + animate(src, alpha = 160, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + set_light(0, 0, 0) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/attack_hand(mob/living/user) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(user, span_warning("You aren't able to invoke these symbols.")) + return + + if(siphon_active) + deactivate_siphon() + to_chat(user, span_cultsmall("The siphon stills.")) + else + activate_siphon(user) + return ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/proc/activate_siphon(mob/living/user) + siphon_active = TRUE + activating_mob = user + set_active_visuals(TRUE) + user.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") + playsound(src, 'sound/magic/cosmic_expansion.ogg', 50, TRUE) + to_chat(user, span_hierophant_warning("The siphon awakens, it will drink from what it can find.")) + + //this is mainly so we don't need 100 different processing subsystems + process_timer = addtimer(CALLBACK(src, PROC_REF(siphon_process)), loop_speed, TIMER_LOOP | TIMER_STOPPABLE) + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/proc/deactivate_siphon() + siphon_active = FALSE + activating_mob = null + if(process_timer) + deltimer(process_timer) + set_active_visuals(FALSE) + do_invoke_glow() + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return + + if(!user_facing) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if(!isliving(user)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/mob/living/L = user + if(GET_MOB_SKILL_VALUE(L, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(L, span_warning("You aren't able to commune with these symbols.")) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if(!L.get_bleed_rate()) + to_chat(L, span_hierophant_warning("The rune demands a blood-price, you must be bleeding to bind yourself to it.")) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if(L in subscribers) + subscribers -= L + to_chat(L, span_cultsmall("You withdraw your bond from the siphon.")) + L.remove_status_effect(buff) + else + subscribers += L + playsound(src, 'sound/magic/glass.ogg', 50, TRUE) + to_chat(L, span_hierophant_warning("Your blood seals the bond, the siphon will feed you while you remain close.")) + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/proc/siphon_process() + if(!siphon_active || QDELETED(src)) + deactivate_siphon() + return + + var/mana_found = drain_mana() + if(!mana_found) + if(activating_mob && !QDELETED(activating_mob)) + to_chat(activating_mob, span_hierophant_warning("The siphon finds nothing to drink, it falls dark.")) + deactivate_siphon() + return + + trigger_effects() + // Small visual pulse to show the rune is working + animate(src, alpha = 220, time = 0.2 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.2 SECONDS) + animate(src, alpha = 255, time = 0.2 SECONDS, flags = ANIMATION_END_NOW) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/proc/drain_mana() + var/mana_found = FALSE + var/turf/here = get_turf(src) + for(var/atom/movable/thing as anything in here) + if(thing == src) + continue + if(isnull(thing.mana_pool)) + continue + var/datum/mana_pool/pool = thing.mana_pool + if(pool.amount <= mana_cost * max(1, length(subscribers))) + continue + var/drain = min(pool.amount, mana_cost * max(1, length(subscribers))) + pool.adjust_mana(-drain) + mana_found = TRUE + break + + if(!mana_found) + for(var/obj/structure/mana_pylon/pylon in range(runesize, src)) + if(pylon.mana_pool.amount <= mana_cost * max(1, length(subscribers))) + continue + var/drain = min(pylon.mana_pool.amount, mana_cost * max(1, length(subscribers))) + pylon.mana_pool.adjust_mana(-drain) + var/turf/turf = get_turf(src) + turf.Beam(pylon, icon_state = "drain_life", time = loop_speed, override_target_pixel_y = 32) + mana_found = TRUE + break + + if(!mana_found) + if(activating_mob && !QDELETED(activating_mob) && activating_mob.stat == CONSCIOUS) + if(!isnull(activating_mob.mana_pool) && activating_mob.mana_pool.amount > mana_cost * max(1, length(subscribers))) + var/drain = min(activating_mob.mana_pool.amount, mana_cost * max(1, length(subscribers))) + activating_mob.mana_pool.adjust_mana(-drain) + if(COOLDOWN_FINISHED(src, drain_message)) + to_chat(activating_mob, span_italics("The siphon draws from your own reserves...")) + COOLDOWN_START(src, drain_message, 45 SECONDS) + mana_found = TRUE + return mana_found + + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/proc/trigger_effects() + var/turf/source_turf = loc + for(var/mob/living/sub as anything in subscribers) + if(QDELETED(sub)) + subscribers -= sub + continue + if(sub.has_status_effect(buff)) + continue + var/dist = source_turf.Distance3D(get_turf(sub)) + if(dist > MANA_SIPHON_BUFF_RANGE) + continue + sub.apply_status_effect(buff, null, src) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/Destroy() + deactivate_siphon() + subscribers.Cut() + return ..() + +/datum/status_effect/mana_siphon_buff + id = "mana_siphon_buff" + alert_type = /atom/movable/screen/alert/status_effect/mana_siphon_buff + duration = MANA_SIPHON_BUFF_DURATION + /// The siphon rune that created this buff (weak ref so we don't hold the rune alive) + var/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/source_rune = null + ///the max distance we can be from our parent + var/max_range = MANA_SIPHON_BUFF_RANGE + +/datum/status_effect/mana_siphon_buff/on_creation(mob/living/afflicted, duration_override, obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/rune) + . = ..() + source_rune = rune + to_chat(afflicted, span_hierophant_warning("The siphon's warmth flows through you.")) + +/datum/status_effect/mana_siphon_buff/on_remove() + . = ..() + to_chat(owner, span_cultsmall("The siphon's warmth fades from you.")) + +/datum/status_effect/mana_siphon_buff/tick() + var/turf/source = source_rune.loc + if(!source_rune.siphon_active) + owner.remove_status_effect(src.type) + return + var/dist = source.Distance3D(get_turf(owner)) + if(dist > max_range) + return + duration = initial(duration) + return + +/atom/movable/screen/alert/status_effect/mana_siphon_buff + name = "Mana Siphon" + desc = "You are bound to a mana siphon rune. While near it, you are fed its stolen power." + icon = 'icons/mob/screen_alert.dmi' + icon_state = "censerbuff" // reuse an existing mana icon; swap as needed + +#undef MANA_SIPHON_BUFF_RANGE +#undef MANA_SIPHON_BASE_DRAIN +#undef MANA_SIPHON_PROCESS_RATE +#undef MANA_SIPHON_BUFF_DURATION diff --git a/code/datums/rituals/runes/arcane/draining/action_speed.dm b/code/datums/rituals/runes/arcane/draining/action_speed.dm new file mode 100644 index 00000000000..fb383df30f5 --- /dev/null +++ b/code/datums/rituals/runes/arcane/draining/action_speed.dm @@ -0,0 +1,28 @@ +/datum/actionspeed_modifier/mana_siphon_haste + variable = TRUE + multiplicative_slowdown = -0.3 + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/haste + name = "arcyne haste sigil" + desc = "A jagged, lightning-etched sigil that floods those bound to it with frenetic arcane energy..." + color = "#F5D76E" + invocation = "Kael'veth un'shar!" + buff = /datum/status_effect/mana_siphon_buff/haste + +/datum/status_effect/mana_siphon_buff/haste + id = "mana_siphon_haste_buff" + alert_type = /atom/movable/screen/alert/status_effect/mana_siphon_buff/haste + +/datum/status_effect/mana_siphon_buff/haste/on_creation(mob/living/afflicted) + . = ..() + afflicted.add_or_update_variable_actionspeed_modifier(/datum/actionspeed_modifier/mana_siphon_haste, multiplicative_slowdown = -0.2) + to_chat(afflicted, span_hierophant_warning("Arcane energy crackles through your limbs, your actions quicken.")) + +/datum/status_effect/mana_siphon_buff/haste/on_remove() + . = ..() + owner.remove_actionspeed_modifier(/datum/actionspeed_modifier/mana_siphon_haste) + to_chat(owner, span_cultsmall("The frenetic energy leaves you.")) + +/atom/movable/screen/alert/status_effect/mana_siphon_buff/haste + name = "Arcyne Haste" + desc = "You are bound to a haste sigil. While near it, arcane energy quickens your every action." diff --git a/code/datums/rituals/runes/arcane/draining/cooking.dm b/code/datums/rituals/runes/arcane/draining/cooking.dm new file mode 100644 index 00000000000..ec3b3cbddb5 --- /dev/null +++ b/code/datums/rituals/runes/arcane/draining/cooking.dm @@ -0,0 +1,15 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/cooking + name = "arcyne cooking sigil" + desc = "A refined firey rune, it aids in the cooking process..." + color = "#ffd103" + invocation = "El'keth un'varja!" + user_facing = FALSE + loop_speed = 1 SECONDS + mana_cost = 0.2 + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/cooking/trigger_effects() + for(var/datum/container_craft_operation/craft in GLOB.active_container_crafts) + var/turf/craft_turf = get_turf(craft.crafter) + if(craft_turf.Distance3D(loc) > 5) + continue + craft.progress += SSprocessing.wait diff --git a/code/datums/rituals/runes/arcane/draining/experience.dm b/code/datums/rituals/runes/arcane/draining/experience.dm new file mode 100644 index 00000000000..d273dd0ccc5 --- /dev/null +++ b/code/datums/rituals/runes/arcane/draining/experience.dm @@ -0,0 +1,26 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/experience + name = "arcyne knowledge sigil" + desc = "A smooth sigil, it stimulates the mind of those it affects.." + color = "#63500c" + invocation = "Near'las nal'see!" + buff = /datum/status_effect/mana_siphon_buff/experience + mana_cost = 1 + +/datum/status_effect/mana_siphon_buff/experience + id = "mana_siphon_exp_buff" + alert_type = /atom/movable/screen/alert/status_effect/mana_siphon_buff/experience + max_range = 8 + +/datum/status_effect/mana_siphon_buff/experience/on_creation(mob/living/afflicted) + . = ..() + ADD_TRAIT(owner, TRAIT_ARCANE_KNOWLEDGE, "[type]") + to_chat(afflicted, span_hierophant_warning("Arcane energy crackles through your brain, you feel smarter.")) + +/datum/status_effect/mana_siphon_buff/experience/on_remove() + . = ..() + REMOVE_TRAIT(owner, TRAIT_ARCANE_KNOWLEDGE, "[type]") + to_chat(owner, span_cultsmall("The frenetic energy leaves you.")) + +/atom/movable/screen/alert/status_effect/mana_siphon_buff/experience + name = "Arcyne Improvement" + desc = "You are bound to a knowledge rune, everything feels easier to you." diff --git a/code/datums/rituals/runes/arcane/draining/flight.dm b/code/datums/rituals/runes/arcane/draining/flight.dm new file mode 100644 index 00000000000..fcd43692e8f --- /dev/null +++ b/code/datums/rituals/runes/arcane/draining/flight.dm @@ -0,0 +1,28 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/flight + name = "arcyne flight sigil" + desc = "A smooth curved sigil, allowing free flight in the area.." + color = "#63500c" + invocation = "Kael'veth nal'see!" + buff = /datum/status_effect/mana_siphon_buff/flight + mana_cost = 1.2 + +/datum/status_effect/mana_siphon_buff/flight + id = "mana_siphon_flight_buff" + alert_type = /atom/movable/screen/alert/status_effect/mana_siphon_buff/haste + max_range = 4 + +/datum/status_effect/mana_siphon_buff/flight/on_creation(mob/living/afflicted) + . = ..() + passtable_on(afflicted, "[type]") + ADD_TRAIT(owner, TRAIT_MOVE_FLYING, "[type]") + to_chat(afflicted, span_hierophant_warning("Arcane energy crackles through your limbs, you feel weightless.")) + +/datum/status_effect/mana_siphon_buff/flight/on_remove() + . = ..() + passtable_off(owner, "[type]") + REMOVE_TRAIT(owner, TRAIT_MOVE_FLYING, "[type]") + to_chat(owner, span_cultsmall("The frenetic energy leaves you.")) + +/atom/movable/screen/alert/status_effect/mana_siphon_buff/flight + name = "Arcyne Flight" + desc = "You are bound to a flight sigil. While near it, arcane energy allows free movement upwards." diff --git a/code/datums/rituals/runes/arcane/draining/plant_growth.dm b/code/datums/rituals/runes/arcane/draining/plant_growth.dm new file mode 100644 index 00000000000..11ab6968510 --- /dev/null +++ b/code/datums/rituals/runes/arcane/draining/plant_growth.dm @@ -0,0 +1,33 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/deweed + name = "arcyne deweed sigil" + desc = "A creeping, root-like sigil that reaches into the earth and tears out what does not belong..." + color = "#6BBF59" + invocation = "Vel'keth un'dara!" + user_facing = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/deweed/trigger_effects() + for(var/obj/structure/soil/S in range(4, src)) + S.deweed() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/crop_growth + name = "arcyne growth sigil" + desc = "A spiraling, vine-etched sigil that breathes life into dormant seeds and urges them skyward..." + color = "#A8D84A" + invocation = "Veth'ara un'kael!" + user_facing = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/crop_growth/trigger_effects() + for(var/obj/structure/soil/S in range(4, src)) + if(S.add_growth(1 MINUTES)) + S.update_appearance(UPDATE_OVERLAYS) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/water + name = "arcyne water sigil" + desc = "A flowing, tide-marked sigil that draws moisture from the air and feeds it to the earth..." + color = "#3FA7D6" + invocation = "Aev'seth un'mora!" + user_facing = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/water/trigger_effects() + for(var/obj/structure/soil/S in range(4, src)) + S.adjust_water(20) diff --git a/code/datums/rituals/runes/arcane/draining/waterwheel.dm b/code/datums/rituals/runes/arcane/draining/waterwheel.dm new file mode 100644 index 00000000000..83c5aa2346a --- /dev/null +++ b/code/datums/rituals/runes/arcane/draining/waterwheel.dm @@ -0,0 +1,31 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/waterwheel + name = "arcyne torrent sigil" + desc = "A churning, current-etched sigil that drives water faster and wrings more power from the wheels bound to it..." + color = "#4A90D9" + invocation = "Aev'kael un'draum!" + user_facing = FALSE + var/list/waterwheels = list() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/waterwheel/activate_siphon(mob/living/user) + . = ..() + set_waterwheel_stress(2) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/waterwheel/deactivate_siphon() + set_waterwheel_stress(0.5) // undo the doubling + return ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/waterwheel/trigger_effects() + set_waterwheel_stress(2, TRUE) + return //literally only cause we can build one midway + +/obj/effect/decal/cleanable/ritual_rune/arcyne/mana_siphon/waterwheel/proc/set_waterwheel_stress(multiplier, ignore_list = FALSE) + for(var/obj/structure/waterwheel/W in range(runesize, src)) + if(ignore_list && (W in waterwheels)) + continue + if(!W.stress_generator || !W.last_stress_generation) + continue + if(multiplier > 1) + waterwheels |= W + else + waterwheels -= W + W.set_stress_generation(W.last_stress_generation * multiplier) diff --git a/code/datums/rituals/runes/arcane/empowerment.dm b/code/datums/rituals/runes/arcane/empowerment.dm new file mode 100644 index 00000000000..11cd119ac27 --- /dev/null +++ b/code/datums/rituals/runes/arcane/empowerment.dm @@ -0,0 +1,25 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/empowerment + name = "empowerment array" + desc = "Arcane symbols pulse upon the ground..." + icon = 'icons/effects/96x96.dmi' + icon_state = "empowerment" + tier = 2 + SET_BASE_PIXEL(-32, -32) + pixel_z = 0 + invocation = "Thal'miren vek'laris un'vethar!" + layer = SIGIL_LAYER + color = "#3A0B61" + can_be_scribed = TRUE + ritual_number = TRUE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/empowerment/get_ritual_list_for_rune() + return tier >= 2 ? GLOB.t2buffrunerituallist : GLOB.buffrunerituallist + +/obj/effect/decal/cleanable/ritual_rune/arcyne/empowerment/invoke(list/invokers, datum/runerituals/buff/runeritual) + if(!..()) + return + for(var/mob/living/nearby_mob in range(runesize, src)) + nearby_mob.apply_status_effect(runeritual.buff) + if(ritual_result) + pickritual.cleanup_atoms(selected_atoms) + finish_invoke(invokers) diff --git a/code/datums/rituals/runes/arcane/etching.dm b/code/datums/rituals/runes/arcane/etching.dm new file mode 100644 index 00000000000..bb4d779bcff --- /dev/null +++ b/code/datums/rituals/runes/arcane/etching.dm @@ -0,0 +1,200 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch + name = "arcyne etching seal" + desc = "A sigil of inscription. Place a blank focus and invoke to etch one of your known spells into it." + icon = 'icons/effects/160x160.dmi' + icon_state = "imbuement" + tier = 1 + runesize = 2 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + invocation = "Ven'sha kael imn'dorath!" + can_be_scribed = TRUE + color = "#3BBE5A" + + /// The blank focus staged on this rune + var/obj/item/spell_focus/staged_focus = null + /// How many charges to etch in + var/chosen_charges = 1 + /// The spell type chosen at invoke time + var/datum/action/cooldown/spell/chosen_spell_type = null + /// TRUE while animating + var/animating = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch/attack_hand(mob/living/user) + if(animating) + to_chat(user, span_notice("The seal is already working...")) + return + if(!user.get_active_held_item()) + if(staged_focus) + try_invoke(user) + else + to_chat(user, span_hierophant_warning("Place a blank arcyne focus on the seal first.")) + return + +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch/attackby(obj/item/W, mob/user, list/modifiers) + if(animating) + to_chat(user, span_notice("The seal is already working...")) + return + if(istype(W, /obj/item/spell_focus)) + var/obj/item/spell_focus/focus = W + if(focus.stored_spell_type) + to_chat(user, span_warning("That focus is already etched with [focus.stored_spell_name].")) + return + if(staged_focus) + to_chat(user, span_notice("A focus is already placed here.")) + return + user.temporarilyRemoveItemFromInventory(W) + W.forceMove(get_turf(src)) + W.anchored = TRUE + staged_focus = focus + animate(W, pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + to_chat(user, span_cultsmall("The focus settles onto the seal...")) + playsound(src, 'sound/magic/glass.ogg', 60, TRUE) + return + return ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch/proc/try_invoke(mob/living/user) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(user, span_warning("You aren't able to invoke these symbols.")) + return + if(rune_in_use) + to_chat(user, span_notice("The seal is already active.")) + return + + // Build eligible spell list, exclude essence and already-temporary spells + var/list/eligible = list() + for(var/datum/action/cooldown/spell/S in user.actions) + if(S.spell_type == SPELL_ESSENCE) + continue + if(S.spell_flags & SPELL_TEMPORARY) + continue + if(S.spell_flags & SPELL_UNETCHABLE) + continue + eligible[S.name] = S + + if(!eligible.len) + to_chat(user, span_warning("You have no storable spells.")) + return + + var/chosen_name = tgui_input_list(user, "Choose a spell to etch:", "Etch Focus", eligible) + if(!chosen_name || !eligible[chosen_name]) + return + if(!staged_focus || QDELETED(staged_focus)) // race guard + return + + var/datum/action/cooldown/spell/chosen = eligible[chosen_name] + + // Ask how many charges (1-3, capped by mana availability) + var/max_possible_charges = min(3, floor(user.mana_pool.amount / max(1, chosen.spell_cost * 2))) + if(max_possible_charges < 1) + to_chat(user, span_hierophant_warning("You don't have enough mana to etch even one charge of [chosen.name].")) + return + + var/charge_options = list() + for(var/i = 1, i <= max_possible_charges, i++) + charge_options += "[i] (costs [chosen.spell_cost * 2 * i] mana)" + + var/charge_choice = tgui_input_list(user, "How many charges?", "Etch Focus", charge_options) + if(!charge_choice || !staged_focus || QDELETED(staged_focus)) + return + + // Parse the chosen number back out + chosen_charges = text2num(charge_choice) + chosen_spell_type = chosen.type + + rune_in_use = TRUE + animating = TRUE + + user.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") + playsound(src, 'sound/magic/cosmic_expansion.ogg', 60, TRUE) + SpinAnimation(speed = 1.5 SECONDS, loops = 1, clockwise = TRUE, segments = 6, parallel = TRUE) + + INVOKE_ASYNC(src, PROC_REF(run_etch_animation), user) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch/proc/run_etch_animation(mob/living/user) + animate(staged_focus, + transform = matrix() * 0.3, + alpha = 80, + time = 1.0 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(1.0 SECONDS) + + var/success = do_etch(user) + + animate(staged_focus, + transform = matrix(), + alpha = 255, + time = 0.5 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(0.5 SECONDS) + + if(success) + playsound(src, 'sound/magic/blink.ogg', 80, TRUE) + else + for(var/i in 1 to 3) + animate(staged_focus, pixel_x = -4, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + animate(staged_focus, pixel_x = 4, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + animate(staged_focus, pixel_x = 0, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + + staged_focus.anchored = FALSE + animate(staged_focus, pixel_x = 0, pixel_y = 0, time = 0.3 SECONDS, flags = ANIMATION_END_NOW) + staged_focus = null + chosen_charges = 1 + chosen_spell_type = null + + animating = FALSE + rune_in_use = FALSE + do_invoke_glow() + +/// Handles multi-charge mana deduction. +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch/proc/do_etch(mob/living/user) + // Find the live spell again (user may have lost it between input and animation unfortunately) + var/datum/action/cooldown/spell/live = null + for(var/datum/action/cooldown/spell/S in user.actions) + if(S.type == chosen_spell_type) + live = S + break + if(!live) + to_chat(user, span_warning("You no longer know that spell.")) + return FALSE + + var/total_mana = live.spell_cost * 2 * chosen_charges + if(user.mana_pool.amount < total_mana) + to_chat(user, span_phobia("You no longer have enough mana ([total_mana] needed).")) + return FALSE + + user.mana_pool.adjust_mana(-total_mana) + live.StartCooldown() + + staged_focus.stored_spell_type = chosen_spell_type + staged_focus.stored_spell_name = live.name + staged_focus.spell_tier = initial(live.spell_tier) + staged_focus.grant_charges = chosen_charges + staged_focus.name = "[live.name] focus" + staged_focus.desc = "A focus etched with [live.name]. It can be consumed by an imbuing seal." + staged_focus.update_appearance(UPDATE_OVERLAYS | UPDATE_NAME | UPDATE_DESC) + + user.visible_message( + span_notice("[user] traces the seal, the focus blazes with [live.name]!"), + span_notice("You etch [chosen_charges] charge\s of [live.name] into the focus.") + ) + return TRUE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch/proc/abort_etch() + if(staged_focus && !QDELETED(staged_focus)) + staged_focus.anchored = FALSE + animate(staged_focus, pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + staged_focus = null + chosen_charges = 1 + chosen_spell_type = null + animating = FALSE + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/focus_etch/Destroy() + abort_etch() + return ..() diff --git a/code/datums/rituals/runes/arcane/greater_wall.dm b/code/datums/rituals/runes/arcane/greater_wall.dm new file mode 100644 index 00000000000..ca38566c104 --- /dev/null +++ b/code/datums/rituals/runes/arcane/greater_wall.dm @@ -0,0 +1,33 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/wallgreater + name = "fortress accession matrix" + desc = "A massive sigil — is that a wall in the center?" + icon = 'icons/effects/160x160.dmi' + icon_state = "wall" + tier = 3 + invocation = "Thar'morak dul'vorr keth'alor!" + runesize = 2 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + can_be_scribed = TRUE + associated_ritual = /datum/runerituals/wall/t3 + var/datum/map_template/template + var/fortress_template_type = /datum/map_template/arcyne_fortress + +/obj/effect/decal/cleanable/ritual_rune/arcyne/wallgreater/proc/load_fortress_template() + var/datum/map_template/temp = new fortress_template_type() + template = SSmapping.map_templates[temp.id] + if(!template) + WARNING("Fortress template ([temp.id]) not found!") + qdel(src) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/wallgreater/invoke(list/invokers, datum/runerituals/runeritual) + runeritual = associated_ritual + if(!..()) + return + if(QDELETED(src)) + return + load_fortress_template() + template.load(get_turf(src), centered = TRUE) + if(ritual_result) + pickritual.cleanup_atoms(selected_atoms) + finish_invoke(invokers) diff --git a/code/datums/rituals/runes/arcane/knowledge.dm b/code/datums/rituals/runes/arcane/knowledge.dm new file mode 100644 index 00000000000..3b6e7ade273 --- /dev/null +++ b/code/datums/rituals/runes/arcane/knowledge.dm @@ -0,0 +1,29 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/knowledge + name = "Knowledge rune" + desc = "Arcane symbols pulse upon the ground..." + icon_state = "6" + invocation = "Thal'miren vek'laris un'vethar!" + color = "#3A0B61" + spellbonus = 15 + scribe_damage = 10 + can_be_scribed = TRUE + associated_ritual = /datum/runerituals/knowledge + +/obj/effect/decal/cleanable/ritual_rune/arcyne/knowledge/invoke(list/invokers, datum/runerituals/runeritual) + runeritual = associated_ritual + if(!..()) + return + var/mob/living/user = usr + user.apply_status_effect(/datum/status_effect/buff/magicknowledge) + if(ritual_result) + pickritual.cleanup_atoms(selected_atoms) + finish_invoke(invokers) + +/datum/runerituals/knowledge + name = "knowledge gain" + tier = 1 + blacklisted = FALSE + required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 1) + +/datum/runerituals/knowledge/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + return TRUE diff --git a/code/datums/rituals/runes/arcane/leylines.dm b/code/datums/rituals/runes/arcane/leylines.dm new file mode 100644 index 00000000000..fe62bcfe577 --- /dev/null +++ b/code/datums/rituals/runes/arcane/leylines.dm @@ -0,0 +1,44 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/leylines + name = "leyline attunement matrix" + desc = "Geometric shapes and lines on the ground resonate with power..." + icon = 'icons/effects/96x96.dmi' + icon_state = "empowerment" + runesize = 1 + tier = 2 + SET_BASE_PIXEL(-32, -32) + pixel_z = 0 + invocation = "Thal'miren vek'laris un'vethar!" + color = "#a70808ce" + scribe_damage = 10 + can_be_scribed = TRUE + associated_ritual = /datum/runerituals/leyattunement + +/obj/effect/decal/cleanable/ritual_rune/arcyne/leylines/invoke(list/invokers, datum/runerituals/runeritual) + runeritual = associated_ritual + if(!..()) + return + var/mob/living/user = usr + if(user.mana_pool.intrinsic_recharge_sources & MANA_SOULS) + to_chat(user, span_warning("I cannot attune to leylines now.")) + else if(user.mana_pool.intrinsic_recharge_sources & MANA_ALL_LEYLINES) + to_chat(user, span_warning("Already attuned to leylines!")) + else + user.mana_pool.set_intrinsic_recharge(MANA_ALL_LEYLINES) + playsound(user, 'sound/magic/blink.ogg', 80, FALSE) + to_chat(user, span_warning("Leylines fill me with power!")) + if(ritual_result) + pickritual.cleanup_atoms(selected_atoms) + finish_invoke(invokers) + +/datum/runerituals/leyattunement + name = "leyline attunement" + tier = 1 + blacklisted = FALSE + required_atoms = list( + /obj/item/mana_battery/mana_crystal/small = 1, + /obj/item/reagent_containers/food/snacks/produce/manabloom = 2, + /obj/item/natural/leyline = 1 + ) + +/datum/runerituals/leyattunement/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + return TRUE diff --git a/code/datums/rituals/runes/arcane/spell_imbuement.dm b/code/datums/rituals/runes/arcane/spell_imbuement.dm new file mode 100644 index 00000000000..b74c673c2de --- /dev/null +++ b/code/datums/rituals/runes/arcane/spell_imbuement.dm @@ -0,0 +1,149 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue + name = "arcyne imbuing seal" + desc = "A sigil of binding. Place a spell object and invoke your knowledge to seal a spell within." + icon = 'icons/effects/160x160.dmi' + icon_state = "imbuement" + tier = 2 + runesize = 2 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + invocation = "Keth'avar sul'dorath imn!" + can_be_scribed = TRUE + color = "#2F7BBE" + + /// The spellobject placed on the rune + var/obj/item/arcyne_spellobject/staged_object = null + /// The spell focus placed on the rune + var/obj/item/spell_focus/staged_focus = null + /// TRUE while the animation plays + var/animating = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue/attack_hand(mob/living/user) + if(animating) + to_chat(user, span_notice("The seal is already working...")) + return + if(!user.get_active_held_item()) + if(staged_object && staged_focus) + try_invoke(user) + else + var/missing = list() + if(!staged_object) missing += "a spell object" + if(!staged_focus) missing += "a spell focus" + to_chat(user, span_hierophant_warning("The seal needs [english_list(missing)] to proceed.")) + return + +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue/attackby(obj/item/W, mob/user, list/modifiers) + if(animating) + to_chat(user, span_notice("The seal is already working...")) + return + if(istype(W, /obj/item/arcyne_spellobject)) + if(staged_object) + to_chat(user, span_notice("A spell object is already placed here.")) + return + user.temporarilyRemoveItemFromInventory(W) + W.forceMove(get_turf(src)) + W.anchored = TRUE + staged_object = W + animate(W, pixel_x = -16, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + to_chat(user, span_cultsmall("The [W.name] settles onto the seal...")) + playsound(src, 'sound/magic/glass.ogg', 60, TRUE) + return + if(istype(W, /obj/item/spell_focus)) + if(staged_focus) + to_chat(user, span_notice("A spell focus is already placed here.")) + return + user.temporarilyRemoveItemFromInventory(W) + W.forceMove(get_turf(src)) + W.anchored = TRUE + staged_focus = W + animate(W, pixel_x = 16, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + to_chat(user, span_cultsmall("The [W.name] settles onto the seal...")) + playsound(src, 'sound/magic/glass.ogg', 60, TRUE) + return + return ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue/proc/try_invoke(mob/living/user) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(user, span_warning("You aren't able to invoke these symbols.")) + return + if(rune_in_use) + to_chat(user, span_notice("The seal is already active.")) + return + rune_in_use = TRUE + animating = TRUE + + user.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") + playsound(src, 'sound/magic/cosmic_expansion.ogg', 60, TRUE) + SpinAnimation(speed = 1.5 SECONDS, loops = 1, clockwise = TRUE, segments = 6, parallel = TRUE) + + INVOKE_ASYNC(src, PROC_REF(run_imbue_animation), user) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue/proc/run_imbue_animation(mob/living/user) + animate(staged_focus, + pixel_x = 0, pixel_y = 0, + alpha = 0, + transform = matrix() * 0.2, + time = 1.5 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(1.5 SECONDS) + + var/success = staged_object.imbue_spell( + user, + staged_focus.stored_spell_type, + staged_focus.spell_tier, + staged_focus.grant_charges + ) + + if(success) + qdel(staged_focus) + staged_focus = null + animate(staged_object, + transform = matrix() * 1.3, + time = 0.3 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(0.3 SECONDS) + animate(staged_object, + transform = matrix(), + time = 0.3 SECONDS, + flags = ANIMATION_END_NOW + ) + sleep(0.3 SECONDS) + playsound(src, 'sound/magic/blink.ogg', 80, TRUE) + else + for(var/i in 1 to 3) + animate(staged_object, pixel_x = -4, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + animate(staged_object, pixel_x = 4, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + animate(staged_object, pixel_x = 0, time = 0.1 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.1 SECONDS) + + staged_object.anchored = FALSE + animate(staged_object, pixel_x = 0, pixel_y = 0, time = 0.4 SECONDS, flags = ANIMATION_END_NOW) + staged_object = null + if(staged_focus) + staged_focus.anchored = FALSE + staged_focus.alpha = 255 + staged_focus = null + + animating = FALSE + rune_in_use = FALSE + do_invoke_glow() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue/proc/abort_imbue() + if(staged_focus && !QDELETED(staged_focus)) + staged_focus.anchored = FALSE + animate(staged_focus, pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + if(staged_object && !QDELETED(staged_object)) + staged_object.anchored = FALSE + animate(staged_object, pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + staged_focus = null + staged_object = null + animating = FALSE + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/spellobject_imbue/Destroy() + abort_imbue() + return ..() diff --git a/code/datums/rituals/runes/arcane/summoning.dm b/code/datums/rituals/runes/arcane/summoning.dm new file mode 100644 index 00000000000..5afe9f2f5ca --- /dev/null +++ b/code/datums/rituals/runes/arcane/summoning.dm @@ -0,0 +1,91 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning + name = "confinement matrix" + desc = "A relatively basic confinement matrix used to hold small things when summoned." + ritual_number = TRUE + invocation = "Rhegal vex'ultraa!" + tier = 1 + can_be_scribed = TRUE + var/summoning = FALSE + var/mob/living/simple_animal/summoned_mob + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/Destroy() + if(summoning) + release_summon() + .=..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/attack_hand(mob/living/user) + if(summoning && GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) + to_chat(user, span_warning("You release the summon from its containment!")) + playsound(usr, 'sound/magic/teleport_diss.ogg', 75, TRUE) + do_invoke_glow() + sleep(20) + animate(summoned_mob, color = null, time = 5) + release_summon() + return + . = ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/proc/release_summon() + if(!summoned_mob) + return + REMOVE_TRAIT(summoned_mob, TRAIT_PACIFISM, MAGIC_TRAIT) + summoned_mob.status_flags -= GODMODE + summoned_mob.candodge = TRUE + summoned_mob.binded = FALSE + summoned_mob.SetParalyzed(0) + summoned_mob = null + summoning = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/get_ritual_list_for_rune() + switch(tier) + if(1) + return GLOB.t1summoningrunerituallist + if(2) + return GLOB.t2summoningrunerituallist + if(3) + return GLOB.t3summoningrunerituallist + return GLOB.t4summoningrunerituallist + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/invoke(list/invokers, datum/runerituals/runeritual) + if(!..()) + return + if(ismob(ritual_result)) + summoned_mob = ritual_result + summoning = TRUE + if(ritual_result) + pickritual.cleanup_atoms(selected_atoms) + finish_invoke(invokers) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/mid + name = "sealate confinement matrix" + desc = "An adept confinement matrix improved with the addition of a sealate matrix; used to hold things when summoned." + icon = 'icons/effects/96x96.dmi' + icon_state = "sealate" + runesize = 1 + tier = 2 + SET_BASE_PIXEL(-32, -32) + pixel_z = 0 + can_be_scribed = TRUE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/adv + name = "warded sealate confinement matrix" + desc = "A thoroughly-warded confinement matrix improved with the addition of a sealate matrix; used to hold larger, dangerous things when summoned." + icon = 'icons/effects/160x160.dmi' + icon_state = "warded" + runesize = 2 + tier = 3 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + can_be_scribed = TRUE + + +/obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/max + name = "noc's eye warded sealate confinement matrix" + desc = "A thoroughly-warded confinement matrix improved with a Noc's eye sealing measure and the addition of a sealate matrix; used to hold the largest, most dangerous things summonable." + icon = 'icons/effects/224x224.dmi' + icon_state = "huge_runeblued" + runesize = 3 + req_invokers = 3 + tier = 4 + SET_BASE_PIXEL(-96, -96) + pixel_z = 0 + can_be_scribed = TRUE diff --git a/code/datums/rituals/runes/arcane/teleport.dm b/code/datums/rituals/runes/arcane/teleport.dm new file mode 100644 index 00000000000..02bbd6e9128 --- /dev/null +++ b/code/datums/rituals/runes/arcane/teleport.dm @@ -0,0 +1,114 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/teleport + name = "planar convergence matrix" + desc = "A large spiraling sigil that seems to thrum with power." + icon = 'icons/effects/160x160.dmi' + icon_state = "portal" + tier = 2 + req_invokers = 2 + invocation = "Xel'tharr un'korel!" + req_keyword = TRUE + runesize = 2 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + can_be_scribed = TRUE + associated_ritual = /datum/runerituals/teleport + var/listkey + +/obj/effect/decal/cleanable/ritual_rune/arcyne/teleport/Initialize(mapload, set_keyword) + . = ..() + var/area/A = get_area(src) + var/locname = initial(A.name) + listkey = set_keyword ? "[set_keyword] [locname]" : "[locname]" + LAZYADD(GLOB.teleport_runes, src) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/teleport/Destroy() + LAZYREMOVE(GLOB.teleport_runes, src) + return ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/teleport/invoke(list/invokers, datum/runerituals/runeritual) + runeritual = associated_ritual + if(!..()) + return + + var/mob/living/user = invokers[1] + + var/list/potential_runes = list() + var/list/seen_keys = list() + for(var/obj/effect/decal/cleanable/ritual_rune/arcyne/teleport/T as anything in GLOB.teleport_runes) + if(T == src) + continue + potential_runes[avoid_assoc_duplicate_keys(T.listkey, seen_keys)] = T + + if(!length(potential_runes)) + to_chat(user, span_warning("There are no valid runes to teleport to!")) + log_game("Teleport rune activated by [user] at [COORD(src)] failed - no other teleport runes.") + fail_invoke() + return + + var/chosen_key = input(user, "Rune to teleport to", "Teleportation Target") as null|anything in potential_runes + if(isnull(chosen_key)) + return + var/obj/effect/decal/cleanable/ritual_rune/arcyne/teleport/dest = potential_runes[chosen_key] + if(!dest || !Adjacent(user) || QDELETED(src)) + fail_invoke() + return + + var/turf/target = get_turf(dest) + if(target.is_blocked_turf(TRUE)) + to_chat(user, span_warning("The target rune is blocked. Attempting to teleport to it would be massively unwise.")) + log_game("Teleport rune activated by [user] at [COORD(src)] failed - destination blocked.") + fail_invoke() + return + + if(ritual_result) + pickritual.cleanup_atoms(selected_atoms) + + var/moved_anything = FALSE + var/move_user_last = FALSE + var/any_success = FALSE + + for(var/atom/movable/A in range(runesize, src)) + if(istype(A, /obj/effect/dummy/phased_mob)) + continue + if(ismob(A) && !isliving(A)) + continue + if(A == user) + move_user_last = TRUE + moved_anything = TRUE + continue + if(!A.anchored) + moved_anything = TRUE + if(do_teleport(A, target, channel = TELEPORT_CHANNEL_CULT)) + any_success = TRUE + + if(!moved_anything) + fail_invoke() + return + + playsound(src, 'sound/magic/cosmic_expansion.ogg', 50, TRUE) + playsound(target, 'sound/magic/cosmic_expansion.ogg', 50, TRUE) + + if(move_user_last && do_teleport(user, target, channel = TELEPORT_CHANNEL_CULT)) + any_success = TRUE + + if(any_success) + visible_message(span_warning("There is a sharp crack of inrushing air, and everything above the rune disappears!"), null, "You hear a sharp crack.") + to_chat(user, span_cult("You[move_user_last ? "r vision blurs, and with a falling feeling you suddenly appear somewhere else" : " send everything above the rune away"].")) + target.visible_message(span_warning("There is a boom of outrushing air as something appears above the rune!"), null, "You hear a boom.") + else + to_chat(user, span_cult("You[move_user_last ? "r vision blurs briefly, but nothing happens" : " try send everything above the rune away, but the teleportation fails"].")) + + finish_invoke(invokers) + +/datum/runerituals/teleport + name = "planar convergence" + tier = 3 + blacklisted = FALSE + required_atoms = list( + /obj/item/natural/artifact = 1, + /obj/item/natural/leyline = 1, + /obj/item/natural/melded/t2 = 1 + ) + +/datum/runerituals/teleport/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + return TRUE diff --git a/code/datums/rituals/runes/arcane/wall.dm b/code/datums/rituals/runes/arcane/wall.dm new file mode 100644 index 00000000000..29c53027016 --- /dev/null +++ b/code/datums/rituals/runes/arcane/wall.dm @@ -0,0 +1,63 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/wall + name = "wall accession matrix" + desc = "Arcane symbols litter the ground — is that a wall of some sort?" + icon_state = "wall" + tier = 2 + invocation = "Fren'aleth ar'quor!" + ritual_number = TRUE + can_be_scribed = TRUE + color = "#184075" + var/list/barriers = list() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/wall/Destroy() + QDEL_LIST_CONTENTS(barriers) + barriers = null + return ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/wall/attack_hand(mob/living/user) + if(active) + QDEL_LIST_CONTENTS(barriers) + to_chat(user, span_warning("You deactivate the [src]!")) + playsound(usr, 'sound/magic/teleport_diss.ogg', 75, TRUE) + active = FALSE + return + . = ..() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/wall/get_ritual_list_for_rune() + return tier >= 3 ? GLOB.t4wallrunerituallist : GLOB.t2wallrunerituallist + +/obj/effect/decal/cleanable/ritual_rune/arcyne/wall/invoke(list/invokers, datum/runerituals/runeritual) + if(!..()) + return + var/mob/living/user = usr + var/list/turf/targets = wall_get_target_turfs(user, pickritual.tier >= 2) + for(var/turf/T in targets) + if(locate(/obj/structure/forcefield/casted) in T) + continue + barriers += new /obj/structure/forcefield/casted(T, user) + active = TRUE + if(ritual_result) + pickritual.cleanup_atoms(selected_atoms) + finish_invoke(invokers) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/wall/proc/wall_get_target_turfs(mob/living/user, double_row = FALSE) + var/user_dir = user.dir + var/turf/front = get_step(get_step(src, user_dir), user_dir) + var/list/turfs = list( + front, + get_step(front, turn(user_dir, 90)), + get_step(front, turn(user_dir, -90)), + get_step(get_step(front, turn(user_dir, 90)), turn(user_dir, 90)), + get_step(get_step(front, turn(user_dir, -90)), turn(user_dir, -90)) + ) + if(!double_row) + return turfs + var/turf/back = get_step(front, user_dir) + turfs += list( + back, + get_step(back, turn(user_dir, 90)), + get_step(back, turn(user_dir, -90)), + get_step(get_step(back, turn(user_dir, 90)), turn(user_dir, 90)), + get_step(get_step(back, turn(user_dir, -90)), turn(user_dir, -90)) + ) + return turfs diff --git a/code/datums/rituals/runes/rituals/_base.dm b/code/datums/rituals/runes/rituals/_base.dm new file mode 100644 index 00000000000..d7fceb2f6d8 --- /dev/null +++ b/code/datums/rituals/runes/rituals/_base.dm @@ -0,0 +1,47 @@ + +/datum/runerituals + abstract_type = /datum/runerituals + var/category = "Rituals" + var/name + var/desc + /// Assoc list of [typepath or list of typepaths] = count required + var/list/required_atoms = list() + /// Typepaths spawned at the ritual location on success (used by the base on_finished_recipe) + var/list/result_atoms = list() + /// Typepaths that are never matched even if they satisfy a required_atoms entry + var/list/banned_atom_types = list() + /// For summoning rituals: the mob typepath (or instance) to summon + var/mob_to_summon + /// If TRUE, this ritual is hidden from all player-facing lists + var/blacklisted = FALSE + /// Tier of this ritual. Used to gate availability based on the rune's tier. + var/tier = 0 + +/// Called when all ingredients are present and the ritual fires. +/// Return a truthy value on success, FALSE on failure. +/datum/runerituals/proc/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + if(!length(result_atoms)) + return FALSE + for(var/result in result_atoms) + new result(loc) + return TRUE + +/// Returns a display string for a single required item typepath. +/datum/runerituals/proc/parse_required_item(atom/item_path, number_of_things = 1) + if(ispath(item_path, /mob/living/carbon/human)) + return "bod[number_of_things > 1 ? "ies" : "y"]" + if(ispath(item_path, /mob/living)) + return "carcass[number_of_things > 1 ? "es" : ""] of any kind" + return "[initial(item_path.name)]\s" + +/** + * Deletes all non-living atoms in selected_atoms after a successful ritual. + * To preserve a specific atom, remove it from selected_atoms before calling. + */ +/datum/runerituals/proc/cleanup_atoms(list/selected_atoms) + SHOULD_CALL_PARENT(TRUE) + for(var/atom/A as anything in selected_atoms) + if(isliving(A)) + continue + selected_atoms -= A + qdel(A) diff --git a/code/datums/rituals/runes/rituals/buffs.dm b/code/datums/rituals/runes/rituals/buffs.dm new file mode 100644 index 00000000000..343d896030c --- /dev/null +++ b/code/datums/rituals/runes/rituals/buffs.dm @@ -0,0 +1,80 @@ +/datum/runerituals/buff + blacklisted = TRUE + tier = 1 + /// Status effect typepath applied to all invokers in range when the ritual fires + var/buff + +/datum/runerituals/buff/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + return TRUE + +/datum/runerituals/buff/lesserstrength + name = "lesser arcane augmentation of strength" + buff = /datum/status_effect/buff/magicstrength/lesser + blacklisted = FALSE + required_atoms = list(/obj/item/natural/elementalmote = 2, /obj/item/mana_battery/mana_crystal/small = 1) + +/datum/runerituals/buff/lesserconstitution + name = "lesser fortify constitution" + buff = /datum/status_effect/buff/magicconstitution/lesser + blacklisted = FALSE + required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 1, /obj/item/natural/obsidian = 2) + +/datum/runerituals/buff/lesserspeed + name = "lesser haste" + buff = /datum/status_effect/buff/magicspeed/lesser + blacklisted = FALSE + required_atoms = list(/obj/item/natural/artifact = 1, /obj/item/natural/leyline = 1) + +/datum/runerituals/buff/lesserperception + name = "lesser arcane eyes" + buff = /datum/status_effect/buff/magicperception/lesser + blacklisted = FALSE + required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 1, /obj/item/natural/infernalash = 2) + +/datum/runerituals/buff/lesserendurance + name = "lesser vitalized endurance" + buff = /datum/status_effect/buff/magicendurance/lesser + blacklisted = FALSE + required_atoms = list(/obj/item/natural/obsidian = 1, /obj/item/natural/fairydust = 2) + +/datum/runerituals/buff/nightvision + name = "darksight" + buff = /datum/status_effect/buff/darkvision + blacklisted = FALSE + required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 2, /obj/item/natural/iridescentscale = 1, /obj/item/natural/elementalshard = 1) + +/datum/runerituals/buff/strength + name = "arcane augmentation of strength" + buff = /datum/status_effect/buff/magicstrength + tier = 2 + blacklisted = FALSE + required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 2, /obj/item/natural/elementalshard = 2) + +/datum/runerituals/buff/constitution + name = "fortify constitution" + buff = /datum/status_effect/buff/magicconstitution + tier = 2 + blacklisted = FALSE + required_atoms = list(/obj/item/mana_battery/mana_crystal/small = 2, /obj/item/natural/obsidian = 4) + +/datum/runerituals/buff/speed + name = "haste" + buff = /datum/status_effect/buff/magicspeed + tier = 2 + blacklisted = FALSE + required_atoms = list(/obj/item/natural/artifact = 2, /obj/item/natural/leyline = 2) + +/datum/runerituals/buff/perception + name = "arcane eyes" + buff = /datum/status_effect/buff/magicperception + tier = 2 + blacklisted = FALSE + required_atoms = list(/obj/item/reagent_containers/food/snacks/produce/manabloom = 2, /obj/item/natural/hellhoundfang = 1) + +/datum/runerituals/buff/endurance + name = "vitalized endurance" + buff = /datum/status_effect/buff/magicendurance + tier = 2 + blacklisted = FALSE + required_atoms = list(/obj/item/natural/obsidian = 2, /obj/item/natural/iridescentscale = 1) + diff --git a/code/datums/rituals/runes/rituals/summoning.dm b/code/datums/rituals/runes/rituals/summoning.dm new file mode 100644 index 00000000000..f548e955a04 --- /dev/null +++ b/code/datums/rituals/runes/rituals/summoning.dm @@ -0,0 +1,159 @@ + +/datum/runerituals/summoning + abstract_type = /datum/runerituals/summoning + name = "summoning ritual (parent)" + desc = "Summoning parent — should not appear in player lists." + blacklisted = TRUE + +/datum/runerituals/summoning/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + return summon_mob(user, loc, mob_to_summon) + +/** + * Spawns and binds the summoned mob at loc, or returns an existing mob instance. + * The binding is removed when the summoning rune is destroyed or clicked by an arcyne. + */ +/datum/runerituals/summoning/proc/summon_mob(mob/living/user, turf/loc, mob_to_summon) + if(isliving(mob_to_summon)) + return mob_to_summon + var/mob/living/simple_animal/summoned = new mob_to_summon(loc) + ADD_TRAIT(summoned, TRAIT_PACIFISM, MAGIC_TRAIT) + summoned.status_flags += GODMODE + summoned.binded = TRUE + summoned.SetParalyzed(90 SECONDS) + summoned.candodge = FALSE + animate(summoned, color = "#ff0000", time = 5) + return summoned + + +/datum/runerituals/summoning/imp + name = "summoning lesser infernal" + desc = "Summons an infernal imp." + blacklisted = FALSE + tier = 1 + required_atoms = list(/obj/item/fertilizer/ash = 2, /obj/item/natural/obsidian = 1) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/imp + +/datum/runerituals/summoning/hellhound + name = "summoning hellhound" + desc = "Summons a hellhound." + blacklisted = FALSE + tier = 2 + required_atoms = list(/obj/item/natural/infernalash = 3, /obj/item/natural/obsidian = 1, /obj/item/natural/melded/t1 = 1) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/hellhound + +/datum/runerituals/summoning/watcher + name = "summoning infernal watcher" + desc = "Summons an infernal watcher." + blacklisted = FALSE + tier = 3 + required_atoms = list(/obj/item/natural/hellhoundfang = 2, /obj/item/natural/obsidian = 1, /obj/item/natural/melded/t2 = 1) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/watcher + +/datum/runerituals/summoning/archfiend + name = "summoning fiend" + desc = "Summons a fiend." + blacklisted = FALSE + tier = 4 + required_atoms = list(/obj/item/natural/moltencore = 1, /obj/item/natural/obsidian = 3, /obj/item/natural/melded/t3 = 1) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/infernal/fiend + + +/datum/runerituals/summoning/sprite + name = "summoning sprite" + desc = "Summons a fae sprite." + blacklisted = FALSE + tier = 1 + required_atoms = list( + /obj/item/reagent_containers/food/snacks/produce/manabloom = 1, + /obj/item/reagent_containers/food/snacks/produce/fruit/jacksberry = 1 + ) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/sprite + +/datum/runerituals/summoning/glimmer + name = "summoning glimmerwing" + desc = "Summons a fae spirit." + blacklisted = FALSE + tier = 2 + required_atoms = list( + /obj/item/reagent_containers/food/snacks/produce/manabloom = 1, + /obj/item/natural/fairydust = 3, + /obj/item/natural/melded/t1 = 1 + ) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/glimmerwing + +/datum/runerituals/summoning/dryad + name = "summoning dryad" + desc = "Summons a dryad." + blacklisted = FALSE + tier = 3 + required_atoms = list( + /obj/item/reagent_containers/food/snacks/produce/manabloom = 2, + /obj/item/natural/iridescentscale = 2, + /obj/item/natural/melded/t2 = 1 + ) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/dryad + +/datum/runerituals/summoning/sylph + name = "summoning sylph" + desc = "Summons an archfae." + blacklisted = FALSE + tier = 4 + required_atoms = list( + /obj/item/reagent_containers/food/snacks/produce/manabloom = 1, + /obj/item/natural/heartwoodcore = 1, + /obj/item/natural/melded/t3 = 1 + ) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/fae/sylph + + +/datum/runerituals/summoning/crawler + name = "summoning elemental crawler" + desc = "Summons a minor elemental." + blacklisted = FALSE + tier = 1 + required_atoms = list(/obj/item/natural/stone = 3, /obj/item/mana_battery/mana_crystal/small = 1) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/crawler + +/datum/runerituals/summoning/warden + name = "summoning elemental warden" + desc = "Summons an elemental." + blacklisted = FALSE + tier = 2 + required_atoms = list( + /obj/item/natural/elementalmote = 3, + /obj/item/mana_battery/mana_crystal/small = 1, + /obj/item/natural/melded/t1 = 1 + ) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/warden + +/datum/runerituals/summoning/behemoth + name = "summoning elemental behemoth" + desc = "Summons a large elemental." + blacklisted = FALSE + tier = 3 + required_atoms = list( + /obj/item/natural/elementalshard = 2, + /obj/item/mana_battery/mana_crystal/small = 1, + /obj/item/natural/melded/t2 = 1 + ) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/behemoth + +/datum/runerituals/summoning/collossus + name = "summoning elemental colossus" + desc = "Summons a huge elemental." + blacklisted = FALSE + tier = 4 + required_atoms = list( + /obj/item/natural/elementalfragment = 1, + /obj/item/mana_battery/mana_crystal/small = 1, + /obj/item/natural/melded/t3 = 1 + ) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/elemental/collossus + +/datum/runerituals/summoning/abberant + name = "summoning aberrant from the void" + desc = "Summons a long-forgotten creature." + blacklisted = FALSE + tier = 4 + required_atoms = list(/obj/item/natural/melded/t5 = 1) + mob_to_summon = /mob/living/simple_animal/hostile/retaliate/voiddragon diff --git a/code/datums/rituals/runes/rituals/walls.dm b/code/datums/rituals/runes/rituals/walls.dm new file mode 100644 index 00000000000..cced5417f6e --- /dev/null +++ b/code/datums/rituals/runes/rituals/walls.dm @@ -0,0 +1,33 @@ +/datum/runerituals/wall + name = "lesser arcyne wall" + tier = 1 + blacklisted = FALSE + required_atoms = list( + /obj/item/natural/elementalmote = 2, + /obj/item/mana_battery/mana_crystal/small = 1, + /obj/item/natural/melded/t1 = 1 + ) + +/datum/runerituals/wall/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + return 1 // Return value used by wall rune to determine single vs double row + +/datum/runerituals/wall/t2 + name = "greater arcyne wall" + tier = 2 + required_atoms = list( + /obj/item/natural/elementalmote = 4, + /obj/item/mana_battery/mana_crystal/small = 2, + /obj/item/natural/melded/t1 = 1 + ) + +/datum/runerituals/wall/t2/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) + return 2 + +/datum/runerituals/wall/t3 + name = "arcyne fortress" + tier = 3 + required_atoms = list( + /obj/item/natural/artifact = 3, + /obj/item/mana_battery/mana_crystal/small = 3, + /obj/item/natural/melded/t3 = 1 + ) diff --git a/code/datums/wounds/_wound.dm b/code/datums/wounds/_wound.dm index 597dfc955dc..596b286f63b 100644 --- a/code/datums/wounds/_wound.dm +++ b/code/datums/wounds/_wound.dm @@ -217,6 +217,8 @@ GLOBAL_LIST_INIT(primordial_wounds, init_primordial_wounds()) return 0 if(dam < min_damage) return 0 + if(deprecise_zone(zone_precise) != affected.body_zone) + return 0 // we are in a weird place if(damage_dividend < min_damage_dividend) if(!(brittle_bonus && HAS_TRAIT(affected, TRAIT_BRITTLE))) // brittle skips the dividend gate return 0 diff --git a/code/game/objects/effects/blood_jaunt.dm b/code/game/objects/effects/blood_jaunt.dm index 1b929c6abe0..0b47bbae14d 100644 --- a/code/game/objects/effects/blood_jaunt.dm +++ b/code/game/objects/effects/blood_jaunt.dm @@ -1,4 +1,3 @@ - /obj/effect/bloodcult_jaunt mouse_opacity = MOUSE_OPACITY_TRANSPARENT icon = 'icons/effects/vampire/96x96.dmi' @@ -9,8 +8,8 @@ pixel_x = -32 pixel_y = -32 animate_movement = 0 - var/atom/movable/rider = null//lone user? - var/list/packed = list()//moving a lot of stuff? + var/atom/movable/rider = null + var/list/packed = list() var/turf/starting = null var/turf/target = null @@ -27,7 +26,6 @@ var/override_target_X = 0 var/override_target_Y = 0 - //update_pixel stuff var/PixelX = 0 var/PixelY = 0 @@ -41,6 +39,8 @@ var/failsafe = 100 + var/arriving = FALSE + /obj/effect/bloodcult_jaunt/New(turf/loc, mob/user, turf/destination, turf/packup, mob/activator) ..() if (!user && !packup && !force_jaunt) @@ -63,7 +63,10 @@ rider = user if (ismob(rider)) var/mob/M = rider - M.see_invisible = SEE_INVISIBLE_LIVING + if(HAS_TRAIT(M, TRAIT_SEE_LEYLINES)) + M.see_invisible = SEE_INVISIBLE_LEYLINES + else + M.see_invisible = SEE_INVISIBLE_LIVING if (packup) var/list/noncult_victims = list() for (var/atom/movable/AM in packup) @@ -85,31 +88,32 @@ packed.Add(AM) if (ismob(AM)) var/mob/M = AM - M.see_invisible = SEE_INVISIBLE_LIVING + if(HAS_TRAIT(M, TRAIT_SEE_LEYLINES)) + M.see_invisible = SEE_INVISIBLE_LEYLINES + else + M.see_invisible = SEE_INVISIBLE_LIVING starting = loc target = destination initial_pixel_x = pixel_x initial_pixel_y = pixel_y - //first of all, if our target is off Z-Level, we're immediately teleporting to the edge of the map closest to the target if (target?.z != z) - move_to_edge() - //quickly making sure that we're not jaunting to where we are + if(is_in_zweb(z, target.z)) + move_via_zweb() + else + move_to_edge() bump_target_check() if (!src||!loc) return - //calculating how many tiles we should have to cross so we can abort the jaunt if we go off-track failsafe = abs(starting.x - target.x) + abs(starting.y - target.y) - //next, let's rotate the jaunter's sprite to face our destination init_angle() - //now, let's launch the jaunter at our target init_jaunt() /obj/effect/bloodcult_jaunt/Destroy() - if (rider) - QDEL_NULL(rider) - if (packed.len > 0) - for(var/atom/A in packed) - qdel(A) + if(rider) + rider.forceMove(target || loc) + rider = null + for(var/atom/movable/AM in packed) + AM.forceMove(target || loc) packed = list() . = ..() @@ -121,6 +125,14 @@ forceMove(get_step(loc, dir)) bump_target_check() +/obj/effect/bloodcult_jaunt/proc/move_via_zweb() + var/turf/T = locate(loc.x, loc.y, target.z) + if(!T) + move_to_edge() + return + starting = T + forceMove(T) + /obj/effect/bloodcult_jaunt/proc/move_to_edge() var/target_x var/target_y @@ -214,7 +226,7 @@ failsafe-- error += distA bump_target_check() - return 0//so that we don't move twice slower in diagonals + return 0 else var/atom/step = get_step(src, dA) if(!step) @@ -252,7 +264,10 @@ process_step() /obj/effect/bloodcult_jaunt/proc/bump_target_check() + if(arriving) + return if (loc == target || failsafe <= 0) + arriving = TRUE playsound(target, 'sound/effects/vampire/cultjaunt_land.ogg', 30, 0, -3) if (force_jaunt) playsound(target, 'sound/effects/vampire/convert_failure.ogg', 30, 0, -1) @@ -260,14 +275,20 @@ rider.forceMove(target) if (ismob(rider)) var/mob/M = rider - M.see_invisible = 0 + if(HAS_TRAIT(M, TRAIT_SEE_LEYLINES)) + M.see_invisible = SEE_INVISIBLE_LEYLINES + else + M.see_invisible = SEE_INVISIBLE_LIVING var/jaunter = FALSE for (var/obj/effect/blood_ritual/seer/seer_ritual in GLOB.seer_rituals) if (seer_ritual.caster == M) jaunter = TRUE break if (!jaunter) - M.see_invisible = 0 + if(HAS_TRAIT(M, TRAIT_SEE_LEYLINES)) + M.see_invisible = SEE_INVISIBLE_LEYLINES + else + M.see_invisible = SEE_INVISIBLE_LIVING rider = null if (packed.len > 0) @@ -275,7 +296,10 @@ AM.forceMove(target) if (ismob(AM)) var/mob/M = AM - M.see_invisible = SEE_INVISIBLE_LIVING + if(HAS_TRAIT(M, TRAIT_SEE_LEYLINES)) + M.see_invisible = SEE_INVISIBLE_LEYLINES + else + M.see_invisible = SEE_INVISIBLE_LIVING for (var/obj/effect/blood_ritual/seer/seer_ritual in GLOB.seer_rituals) if (seer_ritual.caster == M) break @@ -297,3 +321,8 @@ /obj/effect/bloodcult_jaunt/visible invisibility = 0 alpha = 255 + +/obj/effect/bloodcult_jaunt/visible/ley + color = "#86c5ff" + invisibility = 0 + alpha = 255 diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index fcbd7b0199e..e54a7bd6c6a 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -15,6 +15,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e var/lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' ///Icon file for right inhand overlays var/righthand_file = 'icons/mob/inhands/items_righthand.dmi' + ///basically avoiding adding an atom var this is the we are indexed by the recipe book call + var/indexed = FALSE ///Icon file for mob worn overlays. ///no var for state because it should *always* be the same as icon_state @@ -1623,3 +1625,74 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e if(!silent) balloon_alert_to_viewers(span_warning("[name]breaks!")) + + +/obj/item/return_recipe_data() + var/has_grind = length(grind_results) + var/has_juice = length(juice_results) + var/list/milled_from_paths = GLOB.snack_mill_reverse[type] + var/list/sliced_from_paths = GLOB.snack_slice_reverse[type] + + if(!has_grind&& !has_juice && !length(milled_from_paths) && !length(sliced_from_paths)) + return null + + var/list/data = list() + data["type"] = "snack_processing" + data["name"] = name + data["category"] = "Processing" + data["_output_path"] = "[type]" + data["output_name"] = name + data["output_icon"] = "[icon]" + data["output_state"] = "[icon_state]" + + if(has_grind) + var/list/grind = list() + for(var/datum/reagent/path as anything in grind_results) + grind += list(list("name" = initial(path.name), "amount" = grind_results[path])) + data["grind_results"] = grind + + if(has_juice) + var/list/juice = list() + var/list/combined_path = juice_results + for(var/datum/reagent/path as anything in combined_path) + juice += list(list("name" = initial(path.name), "amount" = combined_path[path])) + data["juice_results"] = juice + + if(length(sliced_from_paths)) + var/list/sliced_from = list() + for(var/atom/src_path as anything in sliced_from_paths) + sliced_from += list(list( + "name" = initial(src_path.name), + "icon" = "[initial(src_path.icon)]", + "icon_state" = "[initial(src_path.icon_state)]", + "_path" = "[src_path]", + )) + data["sliced_from"] = sliced_from + + if(length(milled_from_paths)) + var/list/milled_from = list() + for(var/atom/src_path as anything in milled_from_paths) + milled_from += list(list( + "name" = initial(src_path.name), + "icon" = "[initial(src_path.icon)]", + "icon_state" = "[initial(src_path.icon_state)]", + "_path" = "[src_path]", + )) + data["milled_from"] = milled_from + + if(length(obtained_from)) + var/list/sources = list() + for(var/list/entry as anything in obtained_from) + if(!islist(entry) || length(entry) < 2) continue + var/label = entry[1] + var/atom/src_path = entry[2] + sources += list(list( + "label" = label, + "_path" = "[src_path]", + "name" = initial(src_path.name), + "icon" = "[initial(src_path.icon)]", + "icon_state" = "[initial(src_path.icon_state)]", + )) + data["sources"] = sources + + return data diff --git a/code/game/objects/items/arcyne_spellobject.dm b/code/game/objects/items/arcyne_spellobject.dm new file mode 100644 index 00000000000..4cf432ae7dd --- /dev/null +++ b/code/game/objects/items/arcyne_spellobject.dm @@ -0,0 +1,374 @@ +/datum/spellobject_entry + /// Type path of the stored spell + var/datum/action/cooldown/spell/spell_type = null + /// Cached display name + var/spell_name = null + /// Casts remaining + var/charges = 1 + /// Live granted spell instance (passive-grant mode only) + var/datum/action/cooldown/spell/live_spell = null + +/obj/item/arcyne_spellobject + name = "arcyne spell object" + desc = "An object threaded with arcyne filaments." + w_class = WEIGHT_CLASS_SMALL + + grid_width = 64 + grid_height = 32 + /// Maximum number of distinct spell slots + var/max_spells = 3 + /// Minimum accepted spell tier + var/min_spell_tier = 0 + /// Maximum accepted spell tier + var/max_spell_tier = 99 + ///if we hijack a click or obscure + var/spellobject_flags = NONE + /// List of /datum/spellobject_entry + var/list/datum/spellobject_entry/stored_spells = list() + /// TRUE while spells are actively granted (passive mode only) + var/active = FALSE + +/obj/item/arcyne_spellobject/examine(mob/user) + . = ..() + if(!length(stored_spells)) + . += span_warning("It is cold and empty.") + return + . += span_notice("Spells stored within ([length(stored_spells)]/[max_spells]):") + for(var/datum/spellobject_entry/E in stored_spells) + // Chaotic items obscure their spell names + if(spellobject_flags & SPELLOBJECT_CHAOTIC) + . += span_notice(" ??? - [E.charges] charge\s remaining.") + else + . += span_notice(" [E.spell_name] - [E.charges] charge\s remaining.") + if(spellobject_flags & SPELLOBJECT_HIJACK_CLICK) + . += span_notice("It crackles faintly, point and click to unleash its magic.") + if(spellobject_flags & SPELLOBJECT_CHAOTIC) + . += span_warning("The magic within feels wild and unpredictable.") + +/obj/item/arcyne_spellobject/update_overlays() + . = ..() + if(!(spellobject_flags & SPELLOBJECT_VISUAL)) + return + var/i = 0 + for(var/datum/spellobject_entry/E in stored_spells) + var/datum/action/cooldown/spell/S = E.spell_type + var/mutable_appearance/MA = mutable_appearance(initial(S.button_icon), initial(S.button_icon_state)) + MA.alpha = max(40, 120 - i * 20) + MA.pixel_z = i * 2 + . += MA + i++ + +/obj/item/arcyne_spellobject/equipped(mob/user, slot) + . = ..() + if(spellobject_flags & SPELLOBJECT_HIJACK_CLICK) + return + if(!length(stored_spells)) + return + grant_all_spells(user) + +/obj/item/arcyne_spellobject/dropped(mob/user) + . = ..() + if(spellobject_flags & SPELLOBJECT_HIJACK_CLICK) + return + if(active) + revoke_all_spells(user) + +/obj/item/arcyne_spellobject/afterattack(atom/target, mob/living/user, proximity_flag, click_parameters) + if(!(spellobject_flags & SPELLOBJECT_HIJACK_CLICK)) + return ..() + //no proximity check; works at any distance + if(!length(stored_spells)) + to_chat(user, span_warning("Nothing is stored within.")) + return + fire_hijack_spell(user, target) + +/obj/item/arcyne_spellobject/proc/fire_hijack_spell(mob/living/user, mob/living/intended_target) + var/datum/spellobject_entry/E = stored_spells[1] + var/datum/action/cooldown/spell/spell_type = E.spell_type + var/skill_level = GET_MOB_SKILL_VALUE(user, initial(spell_type.associated_skill)) + var/requirement + if(skill_level >= SKILL_LEVEL_LEGENDARY) + requirement = SPELLOBJECT_AIM_REQ_LEGENDARY + else if(skill_level >= SKILL_LEVEL_MASTER) + requirement = SPELLOBJECT_AIM_REQ_MASTER + else if(skill_level >= SKILL_LEVEL_EXPERT) + requirement = SPELLOBJECT_AIM_REQ_EXPERT + else if(skill_level >= SKILL_LEVEL_JOURNEYMAN) + requirement = SPELLOBJECT_AIM_REQ_JOURNEYMAN + else if(skill_level >= SKILL_LEVEL_APPRENTICE) + requirement = SPELLOBJECT_AIM_REQ_APPRENTICE + else if(skill_level >= SKILL_LEVEL_NOVICE) + requirement = SPELLOBJECT_AIM_REQ_NOVICE + else + requirement = SPELLOBJECT_AIM_REQ_NONE + + var/roll_result = user.diceroll(requirement = requirement, crit = 3) + + var/mob/living/actual_target + if(spellobject_flags & SPELLOBJECT_STABLE) + actual_target = intended_target + else + switch(roll_result) + if(DICE_CRIT_SUCCESS) + actual_target = intended_target + user.visible_message( + span_notice("[user] levels [src], a searing bolt lances straight and true!"), + span_notice("The magic responds perfectly [E.spell_name] fires true.") + ) + if(DICE_SUCCESS) + actual_target = intended_target + user.visible_message( + span_notice("[user] levels [src] and a burst of energy lances toward [intended_target]!"), + span_notice("The spell fires toward [intended_target].") + ) + if(DICE_FAILURE) + if(spellobject_flags & SPELLOBJECT_CHAOTIC) + var/list/nearby = get_hearers_in_view(7, user) - user + actual_target = length(nearby) ? pick(nearby) : user + user.visible_message( + span_warning("[user]'s [src] sputters, the magic lurches wildly toward [actual_target]!"), + span_warning("The magic slips your control the spell careens toward [actual_target]!") + ) + else + actual_target = user + user.visible_message( + span_warning("[user]'s [src] sputters, the magic turns back on them!"), + span_warning("The magic slips your control [E.spell_name] snaps back at you!") + ) + if(DICE_CRIT_FAILURE) + if(spellobject_flags & SPELLOBJECT_CHAOTIC) + var/list/wild = get_hearers_in_view(14, user) - user + actual_target = length(wild) ? pick(wild) : user + user.visible_message( + span_boldwarning("[user]'s [src] erupts in wild light, the spell screams toward [actual_target]!"), + span_boldwarning("Catastrophic misfire the spell explodes toward [actual_target]!") + ) + else + actual_target = user + user.visible_message( + span_boldwarning("[user]'s [src] violently misfires, the spell explodes back into them!"), + span_boldwarning("The magic catastrophically misfires [E.spell_name] erupts into you!") + ) + + var/datum/action/cooldown/spell/instance = new E.spell_type(user) + instance.point_cost = 0 + instance.spell_cost = 0 + instance.cooldown_time = 0 + instance.spell_flags |= SPELL_TEMPORARY + instance.Grant(user) + instance.cast(actual_target) + instance.Remove(user) + qdel(instance) + + consume_entry_charge(user, E) + + +/obj/item/arcyne_spellobject/proc/consume_entry_charge(mob/living/user, datum/spellobject_entry/E) + E.charges-- + if(E.charges <= 0) + if(active && E.live_spell) + revoke_entry(user, E) + stored_spells -= E + user.visible_message( + span_notice("A thread of light unravels from [user]'s [name], [E.spell_name] is spent."), + span_notice("The last charge of [E.spell_name] is spent.") + ) + qdel(E) + update_appearance(UPDATE_OVERLAYS) + if(!length(stored_spells)) + if(active) + UnregisterSignal(user, COMSIG_MOB_ABILITY_FINISHED) + active = FALSE + user.visible_message( + span_warning("[user]'s [name] dims — all spells exhausted."), + span_warning("The [name] is now empty.") + ) + if(spellobject_flags & SPELLOBJECT_CONSUMABLE) + qdel(src) + else + to_chat(user, span_notice("[E.spell_name]: [E.charges] charge\s remaining.")) + +/obj/item/arcyne_spellobject/proc/grant_all_spells(mob/user) + if(active) + return + active = TRUE + for(var/datum/spellobject_entry/E in stored_spells) + grant_entry(user, E) + RegisterSignal(user, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(on_spell_fired)) + to_chat(user, span_hierophant_warning("The [name] thrums, [length(stored_spells)] spell\s ready.")) + +/obj/item/arcyne_spellobject/proc/revoke_all_spells(mob/user) + if(!active) + return + UnregisterSignal(user, COMSIG_MOB_ABILITY_FINISHED) + for(var/datum/spellobject_entry/E in stored_spells) + revoke_entry(user, E) + active = FALSE + +/obj/item/arcyne_spellobject/proc/grant_entry(mob/user, datum/spellobject_entry/E) + if(E.live_spell || !E.spell_type) + return + E.live_spell = new E.spell_type(user) + E.live_spell.point_cost = 0 + E.live_spell.cooldown_time = 0 + E.live_spell.spell_cost = 0 + E.live_spell.spell_flags |= SPELL_TEMPORARY + E.live_spell.background_icon_state = "spelltemp" + E.live_spell.base_background_icon_state = "spelltemp0" + E.live_spell.active_background_icon_state = "spelltemp1" + E.live_spell.Grant(user) + +/obj/item/arcyne_spellobject/proc/revoke_entry(mob/user, datum/spellobject_entry/E) + if(!E.live_spell) + return + E.live_spell.Remove(user) + qdel(E.live_spell) + E.live_spell = null + +/obj/item/arcyne_spellobject/proc/on_spell_fired(mob/source, datum/action/cooldown/spell/fired) + SIGNAL_HANDLER + var/datum/spellobject_entry/fired_entry = null + for(var/datum/spellobject_entry/E in stored_spells) + if(E.live_spell == fired) + fired_entry = E + break + if(!fired_entry) + return + consume_entry_charge(source, fired_entry) + +/obj/item/arcyne_spellobject/Moved(atom/old_loc, movement_dir, forced, list/old_locs) + . = ..() + // If passive-grant item is no longer held by the mob that had it, revoke + if(active && !istype(loc, /mob)) + var/mob/M = old_loc + if(istype(M)) + revoke_all_spells(M) + +/obj/item/arcyne_spellobject/proc/imbue_spell(mob/caster, datum/action/cooldown/spell/spell_type_path, spell_tier, charges = 1) + if(length(stored_spells) >= max_spells) + to_chat(caster, span_hierophant_warning("The [name] is already full ([max_spells] spells).")) + return FALSE + if(spell_tier < min_spell_tier || spell_tier > max_spell_tier) + to_chat(caster, span_hierophant_warning("This object cannot hold a spell of that tier.")) + return FALSE + + var/datum/action/cooldown/spell/live = null + for(var/datum/action/cooldown/spell/S in caster.actions) + if(S.type == spell_type_path) + live = S + break + if(!live) + to_chat(caster, span_warning("You don't know that spell, you can't store what you don't have.")) + return FALSE + + var/mana_cost = live.spell_cost * 2 + if(caster.mana_pool.amount < mana_cost) + to_chat(caster, span_phobia("You need [mana_cost] mana to imbue this spell (you have [caster.mana_pool.amount]).")) + return FALSE + caster.mana_pool.adjust_mana(-mana_cost) + live.StartCooldown() + + var/datum/spellobject_entry/E = new() + E.spell_type = spell_type_path + E.spell_name = live.name + E.charges = charges + stored_spells += E + + update_appearance(UPDATE_OVERLAYS) + to_chat(caster, span_hierophant_warning("You pour [mana_cost] mana into the [name], [live.name] is sealed within.")) + return TRUE + +/obj/item/arcyne_spellobject/Destroy() + if(active && istype(loc, /mob)) + revoke_all_spells(loc) + stored_spells.Cut() + return ..() + +/obj/item/arcyne_spellobject/scroll + name = "arcyne scroll" + icon = 'icons/roguetown/items/misc.dmi' + desc = "Dry parchment veined with cold arcyne light. Whatever is written here was not meant to last." + icon_state = "scroll" + max_spells = 1 + w_class = WEIGHT_CLASS_SMALL + spellobject_flags = SPELLOBJECT_HIJACK_CLICK | SPELLOBJECT_CONSUMABLE | SPELLOBJECT_VISUAL | SPELLOBJECT_STABLE + +/obj/item/arcyne_spellobject/scroll/random/Initialize(mapload) + . = ..() + var/datum/action/cooldown/spell/spell_type_path = pick(subtypesof(/datum/action/cooldown/spell)) + while(IS_ABSTRACT(spell_type_path) || initial(spell_type_path.spell_tier) < min_spell_tier || initial(spell_type_path.spell_tier) > max_spell_tier) + spell_type_path = pick(subtypesof(/datum/action/cooldown/spell)) + + var/datum/spellobject_entry/E = new() + E.spell_type = spell_type_path + E.spell_name = initial(spell_type_path.name) + E.charges = rand(1, 3) + stored_spells += E + update_appearance(UPDATE_OVERLAYS) + +/obj/item/arcyne_spellobject/spellstone + name = "arcyne spellstone" + desc = "A polished stone threaded with arcyne filaments. Hold it to channel its spells." + icon = 'icons/roguetown/items/gems.dmi' + icon_state = "quartz" + max_spells = 3 + spellobject_flags = SPELLOBJECT_VISUAL + +/obj/item/arcyne_spellobject/spellstone/lesser + name = "lesser arcyne spellstone" + icon_state = "quartz" + max_spells = 2 + max_spell_tier = 1 + +/obj/item/arcyne_spellobject/spellstone/greater + name = "greater arcyne spellstone" + icon_state = "sapphire" + max_spells = 3 + min_spell_tier = 1 + max_spell_tier = 2 + +/obj/item/arcyne_spellobject/spellstone/supreme + name = "supreme arcyne spellstone" + icon_state = "ruby" + max_spells = 4 + min_spell_tier = 2 + +/obj/item/arcyne_spellobject/wand + name = "arcyne wand" + desc = "A slender wand crackling with stored magic. Point and click to fire." + icon = 'icons/roguetown/items/wands.dmi' + icon_state = "wand_lesser" + w_class = WEIGHT_CLASS_SMALL + spellobject_flags = SPELLOBJECT_HIJACK_CLICK + max_spells = 1 + max_spell_tier = 1 + +/obj/item/arcyne_spellobject/wand/greater + name = "greater arcyne wand" + icon_state = "wand_greater" + max_spells = 2 + min_spell_tier = 1 + max_spell_tier = 2 + +/obj/item/arcyne_spellobject/wand/chaotic + name = "chaotic arcyne wand" + desc = "A warped wand fizzing with wild magic. Something is inside but what?" + spellobject_flags = SPELLOBJECT_HIJACK_CLICK | SPELLOBJECT_CHAOTIC + max_spells = 1 + max_spell_tier = 2 + +/obj/item/arcyne_spellobject/wand/chaotic/random + name = "chaotic arcyne wand" + desc = "A warped wand fizzing with wild magic. Something is inside but what?" + +/obj/item/arcyne_spellobject/wand/chaotic/random/Initialize(mapload) + . = ..() + var/datum/action/cooldown/spell/spell_type_path = pick(subtypesof(/datum/action/cooldown/spell)) + while(IS_ABSTRACT(spell_type_path) || initial(spell_type_path.spell_tier) < min_spell_tier || initial(spell_type_path.spell_tier) > max_spell_tier) + spell_type_path = pick(subtypesof(/datum/action/cooldown/spell)) + + var/datum/spellobject_entry/E = new() + E.spell_type = spell_type_path + E.spell_name = initial(spell_type_path.name) + E.charges = rand(1, 3) + stored_spells += E diff --git a/code/game/objects/items/gems.dm b/code/game/objects/items/gems.dm index 936031e1197..b16b5c830eb 100644 --- a/code/game/objects/items/gems.dm +++ b/code/game/objects/items/gems.dm @@ -18,6 +18,8 @@ var/quality = GEM_REGULAR var/datum/gem_effect/effect_template var/is_cut = FALSE + var/arcyne_potency = 20 + var/datum/attunement/attuned /obj/item/gem/Initialize() . = ..() @@ -174,6 +176,7 @@ icon_state = "emerald_cut" sellprice = 44 dropshrink = 0.4 + arcyne_potency = 7 attuned = /datum/attunement/earth effect_template = /datum/gem_effect/gemerald item_weight = 24 GRAMS @@ -185,6 +188,7 @@ icon_state = "quartz_cut" sellprice = 88 dropshrink = 0.4 + arcyne_potency = 25 attuned = /datum/attunement/ice effect_template = /datum/gem_effect/blortz item_weight = 18 GRAMS @@ -196,6 +200,7 @@ icon_state = "topaz_cut" sellprice = 25 dropshrink = 0.4 + arcyne_potency = 5 attuned = /datum/attunement/electric effect_template = /datum/gem_effect/toper item_weight = 21 GRAMS @@ -207,6 +212,7 @@ icon_state = "sapphire_cut" sellprice = 56 dropshrink = 0.4 + arcyne_potency = 10 attuned = /datum/attunement/arcyne effect_template = /datum/gem_effect/saffira item_weight = 21 GRAMS @@ -218,6 +224,7 @@ icon_state = "diamond_cut" sellprice = 121 dropshrink = 0.4 + arcyne_potency = 15 attuned = /datum/attunement/light effect_template = /datum/gem_effect/dorpel item_weight = 15 GRAMS diff --git a/code/game/objects/items/mageitems.dm b/code/game/objects/items/mageitems.dm index 24637e7b772..9cb55fa323a 100644 --- a/code/game/objects/items/mageitems.dm +++ b/code/game/objects/items/mageitems.dm @@ -99,16 +99,16 @@ return ..() /obj/item/chalk/attack_self(mob/living/carbon/human/user, list/modifiers) - if(!isarcyne(user))//We'll set up other items for other types of rune rituals + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE)//We'll set up other items for other types of rune rituals to_chat(user, span_cult("Nothing comes in mind to draw with the chalk.")) return - var/obj/effect/decal/cleanable/roguerune/pickrune + var/obj/effect/decal/cleanable/ritual_rune/pickrune var/runenameinput = browser_input_list(user, "Runes", "Tier 1&2 Runes", GLOB.t2rune_types) pickrune = GLOB.rune_types[runenameinput] if(!pickrune) return var/turf/Turf = get_turf(user) - if(locate(/obj/effect/decal/cleanable/roguerune) in Turf) + if(locate(/obj/effect/decal/cleanable/ritual_rune) in Turf) to_chat(user, span_cult("There is already a rune here.")) return var/structures_in_way = check_for_structures_and_closed_turfs(loc, pickrune) @@ -130,17 +130,18 @@ if(amount <= 0) qdel(src) -/obj/item/chalk/proc/check_for_structures_and_closed_turfs(loc, obj/effect/decal/cleanable/roguerune/rune_to_scribe) +/obj/item/chalk/proc/check_for_structures_and_closed_turfs(loc, obj/effect/decal/cleanable/ritual_rune/rune_to_scribe) for(var/turf/T in range(loc, rune_to_scribe.runesize)) //check for /sturcture subtypes in the turf's contents for(var/obj/structure/S in T.contents) - return TRUE //Found a structure, no need to continue + if(S.density) + return TRUE //Found a structure, no need to continue //check if turf itself is a /turf/closed subtype if(istype(T,/turf/closed)) return TRUE //check if rune in the turfs contents - for(var/obj/effect/decal/cleanable/roguerune/R in T.contents) + for(var/obj/effect/decal/cleanable/ritual_rune/R in T.contents) return TRUE //Return false if nothing in range was found return FALSE @@ -168,15 +169,15 @@ return ..() /obj/item/weapon/knife/dagger/silver/arcyne/attack_self(mob/living/carbon/human/user, list/modifiers) - if(!isarcyne(user)) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) return - var/obj/effect/decal/cleanable/roguerune/pickrune + var/obj/effect/decal/cleanable/ritual_rune/pickrune var/runenameinput = browser_input_list(user, "Runes", "All Runes", GLOB.t4rune_types) pickrune = GLOB.rune_types[runenameinput] if(!pickrune) return var/turf/Turf = get_turf(user) - if(locate(/obj/effect/decal/cleanable/roguerune) in Turf) + if(locate(/obj/effect/decal/cleanable/ritual_rune) in Turf) to_chat(user, span_cult("There is already a rune here.")) return var/structures_in_way = check_for_structures_and_closed_turfs(loc, pickrune) @@ -207,7 +208,7 @@ span_notice("I finish dragging the blade in symbols and circles, leaving behind a [pickrune.name].")) new pickrune(Turf, chosen_keyword) -/obj/item/weapon/knife/dagger/proc/check_for_structures_and_closed_turfs(loc, obj/effect/decal/cleanable/roguerune/rune_to_scribe) +/obj/item/weapon/knife/dagger/proc/check_for_structures_and_closed_turfs(loc, obj/effect/decal/cleanable/ritual_rune/rune_to_scribe) for(var/turf/T in range(loc, rune_to_scribe.runesize)) //check for /sturcture subtypes in the turf's contents for(var/obj/structure/S in T.contents) @@ -216,7 +217,7 @@ if(istype(T,/turf/closed)) return TRUE //check if rune in the turfs contents - for(var/obj/effect/decal/cleanable/roguerune/R in T.contents) + for(var/obj/effect/decal/cleanable/ritual_rune/R in T.contents) return TRUE //Return false if nothing in range was found return FALSE @@ -644,6 +645,8 @@ w_class = WEIGHT_CLASS_SMALL sellprice = 20 item_weight = 40 GRAMS + var/obj/item/book/granter/spellbook/melded_quality = /obj/item/book/granter/spellbook/adept + var/shock_damage = 20 /obj/item/natural/melded/t1 name = "arcanic meld" @@ -659,6 +662,8 @@ item_flags = OBTAINED_DATA obtained_from = list(list("Killing a Sylph", /mob/living/simple_animal/hostile/retaliate/fae/sylph)) item_weight = 50 GRAMS + melded_quality = /obj/item/book/granter/spellbook/expert + shock_damage = 40 /obj/item/natural/melded/t3 name = "sorcerous weave" @@ -666,6 +671,8 @@ icon_state = "wessence" desc = "A melding of molten core, heartwood core and elemental fragment." item_weight = 60 GRAMS + melded_quality = /obj/item/book/granter/spellbook/master + shock_damage = 60 /obj/item/natural/melded/t4 name = "magical confluence" @@ -673,12 +680,16 @@ icon_state = "wessence" desc = "A melding of abyssal flame, sylvan essence and elemental relic." item_weight = 70 GRAMS + melded_quality = /obj/item/book/granter/spellbook/legendary + shock_damage = 80 /obj/item/natural/melded/t5 name = "arcanic aberation" icon_state = "wessence" desc = "A melding of arcyne fusion and voidstone. It pulses erratically, power coiled tightly within and dangerous. Many would be afraid of going near this, let alone holding it." item_weight = 80 GRAMS + melded_quality = /obj/item/book/granter/spellbook/legendary + shock_damage = 40 /obj/structure/soul name = "soul" @@ -746,3 +757,53 @@ else mana_amount -= transfer_amount user.mana_pool.adjust_mana(transfer_amount) + +/obj/item/pylon_linker + name = "ley linker" + desc = "A mystical tool used to bind mana pylons together, allowing mana to flow between them." + icon = 'icons/roguetown/items/misc.dmi' + icon_state = "dbrush" + + var/obj/structure/mana_pylon/source_pylon + +/obj/item/pylon_linker/afterattack(atom/target, mob/living/user, proximity_flag, list/modifiers) + . = ..() + if(!proximity_flag) + return + if(!istype(target, /obj/structure/mana_pylon)) + return + + var/obj/structure/mana_pylon/pylon = target + + if(!source_pylon) + source_pylon = pylon + user.balloon_alert(user, "source set: [pylon.name]") + return + + if(source_pylon == pylon) + user.balloon_alert(user, "can't link to itself!") + return + + source_pylon.link_pylon(pylon) + user.balloon_alert(user, "pylons linked!") + source_pylon = null + +/obj/item/pylon_linker/afterattack_secondary(atom/target, mob/living/user, proximity_flag, list/modifiers) + . = ..() + if(!proximity_flag) + return + + if(!istype(target, /obj/structure/mana_pylon)) + if(source_pylon) + user.balloon_alert(user, "source cleared!") + source_pylon = null + return + + var/obj/structure/mana_pylon/pylon = target + + if(!pylon.linked_pylon) + user.balloon_alert(user, "not linked!") + return + + pylon.unlink_pylon(pylon.linked_pylon) + user.balloon_alert(user, "link broken!") diff --git a/code/game/objects/items/natural/animals.dm b/code/game/objects/items/natural/animals.dm index 48c78e9b100..2f3189427d4 100644 --- a/code/game/objects/items/natural/animals.dm +++ b/code/game/objects/items/natural/animals.dm @@ -13,6 +13,21 @@ sellprice = 5 item_weight = 350 GRAMS +/obj/item/natural/hide/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) + if(!istype(P, /obj/item/paper/scroll)) + return ..() + if(!isturf(loc) || !locate(/obj/structure/table) in loc) + to_chat(user, "You need to put the [src] on a table to work on it.") + return + var/crafttime = max(0, 100 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) + if(!do_after(user, crafttime, target = src)) + return + playsound(src, 'sound/items/book_close.ogg', 100, TRUE) + to_chat(user, span_notice("I add the first few pages to the leather cover...")) + new /obj/item/spellbook_unfinished(loc) + qdel(P) + qdel(src) + /obj/item/natural/hide/cured name = "cured leather" icon_state = "leather" diff --git a/code/game/objects/items/natural/clothfibersthorn.dm b/code/game/objects/items/natural/clothfibersthorn.dm index 2b5d385919f..8475fe03084 100644 --- a/code/game/objects/items/natural/clothfibersthorn.dm +++ b/code/game/objects/items/natural/clothfibersthorn.dm @@ -88,11 +88,8 @@ resistance_flags = FLAMMABLE max_integrity = 20 item_weight = 3 GRAMS - -/obj/item/natural/thorn/Initialize(mapload) - . = ..() - create_reagents(10) - reagents.add_reagent(/datum/reagent/thorn_essence, 10) + indexed = TRUE + grind_results = list(/datum/reagent/thorn_essence = 10) /obj/item/natural/thorn/attack_self(mob/living/user, list/modifiers) user.visible_message("[user] snaps [src].") diff --git a/code/game/objects/items/natural/wood.dm b/code/game/objects/items/natural/wood.dm index d4e94c505a7..73cdf1ba434 100644 --- a/code/game/objects/items/natural/wood.dm +++ b/code/game/objects/items/natural/wood.dm @@ -137,7 +137,8 @@ grid_height = 64 grid_width = 64 item_weight = 1.4 KILOGRAMS - sap = list(/datum/reagent/tree_sap = 10) + grind_results = list(/datum/reagent/tree_sap = 10) + /obj/item/grown/log/tree/small/apply_components() return @@ -161,7 +162,7 @@ lumber_amount = 0 lumber = null item_weight = 121 GRAMS - sap = null + grind_results = null /obj/item/grown/log/tree/stick/apply_components() return @@ -237,7 +238,7 @@ lumber_amount = 0 tool_behaviour = TOOL_IMPROVISED_RETRACTOR item_weight = 95 GRAMS - sap = null + grind_results = null /obj/item/grown/log/tree/stake/apply_components() return @@ -296,4 +297,4 @@ grid_height = 64 grid_width = 64 item_weight = 100 GRAMS - sap = null + grind_results = null diff --git a/code/game/objects/items/ore.dm b/code/game/objects/items/ore.dm index c0347b29502..6b2809b138c 100644 --- a/code/game/objects/items/ore.dm +++ b/code/game/objects/items/ore.dm @@ -113,6 +113,7 @@ grind_results = list(/datum/reagent/mercury = 15) sellprice = 5 item_weight = 4.2 KILOGRAMS + indexed = TRUE /obj/item/ore/coal/charcoal name = "charcoal" diff --git a/code/game/objects/items/spell_focus.dm b/code/game/objects/items/spell_focus.dm new file mode 100644 index 00000000000..b443aab9d9c --- /dev/null +++ b/code/game/objects/items/spell_focus.dm @@ -0,0 +1,41 @@ +/obj/item/spell_focus + name = "arcyne focus" + desc = "A faceted crystal blank threaded with arcyne filaments. It waits to be etched with a spell." + icon = 'icons/roguetown/items/gems.dmi' + icon_state = "e_cut" + w_class = WEIGHT_CLASS_TINY + /// The spell type etched into this focus, null if blank + var/datum/action/cooldown/spell/stored_spell_type = null + /// Cached spell name + var/stored_spell_name = null + /// Spell tier of the etched spell + var/spell_tier = 0 + /// Charges to grant when consumed by an imbuing rune + var/grant_charges = 1 + +/obj/item/spell_focus/examine(mob/user) + . = ..() + if(stored_spell_type) + . += span_notice("It pulses with stored memory, [stored_spell_name], tier [spell_tier].") + . += span_notice("It will grant [grant_charges] charge\s when imbued.") + else + . += span_warning("It is blank, waiting to be etched.") + +/obj/item/spell_focus/update_overlays() + . = ..() + if(!stored_spell_type) + return + var/mutable_appearance/MA = mutable_appearance(initial(stored_spell_type.button_icon), initial(stored_spell_type.button_icon_state)) + MA.alpha = 100 + . += MA + +/obj/item/spell_focus/random/Initialize(mapload) + . = ..() + stored_spell_type = pick(subtypesof(/datum/action/cooldown/spell)) + if(IS_ABSTRACT(stored_spell_type)) + while(IS_ABSTRACT(stored_spell_type)) + stored_spell_type = pick(subtypesof(/datum/action/cooldown/spell)) + stored_spell_name = initial(stored_spell_type.name) + name = "[initial(stored_spell_type.name)] focus" + desc = "A focus etched with [initial(stored_spell_type.name)]. It can be consumed by an imbuing seal." + update_appearance(UPDATE_OVERLAYS) diff --git a/code/game/objects/items/spellbook.dm b/code/game/objects/items/spellbook.dm index a64863dc29a..d80ca3a3230 100644 --- a/code/game/objects/items/spellbook.dm +++ b/code/game/objects/items/spellbook.dm @@ -1,9 +1,3 @@ -/** - * Spellbooks for learning arcane points. - * Difficult to learn by default, has multipliers for weed, gems and rituals. - * Inherits most of its functionality from default granters, however also has some copy paste from roguebooks for parity. - */ - /obj/item/book/granter/spellbook icon = 'icons/roguetown/items/books.dmi' icon_state = "spellbookbrown_0" @@ -17,93 +11,86 @@ name = "tome of the arcyne" desc = "A crackling, glowing book, filled with runes and symbols that hurt the mind to stare at." pages_to_mastery = 7 - remarks = list("Recall that place of white and black, so cold after its season of heat...", - "Time slips away as I devour each pictograph and sigil...", - "Noc is a shrewd God, and his followers’ writings are no different...", - "The smell of wet rain fills the room with every turned page...", - "Helical text spans the page like a winding puzzle...", - "Tracing a finger over one rune renders my hand paralyzed, if only for a moment...", - "This page clearly details the benefits of swampweed on one's capacity to conceptualize the arcyne...", - "Conceptualize. Theorize. Feel. Flow. Manifest...", - "Passion. Strength. Power. Victory. The tenets through which we break the chains of reality...", - "Magick is to be kept close, a guarded secret. Noc changed the rules again. I need to catch up...", - "Didn’t I just read this page...?", - "A lone illustration of Noc’s visage fills this page, his stony gaze boring into my soul...", - "My eyes begin to lid as I finish this chapter. These symbols cast a heavy fog over my mind...", - "Silver. Blade. Mana. Blood. These are the ingredients I’ll need to imbibe the very ground with arcyne abilities...", - "Elysium incants speak to me in an extinct tongue immortalized on parchment...", - "My mind wanders and waves. Z's temptations draw close, but I weather through as I finally finish this chapter...", - "I close my eye's for but a moment, and the competing visages of Noc and Z stare into my very soul. I see them blink, and my eyelids open...", - "I am the Root. The Root is me. I must reach it, and the Tree...", - "I feel the arcyne circuits running through my body, empowered with each word I read...", - "Am I reading? Are these words, symbols or inane scribbles? I cannot be sure, yet with each one my eyes glaze over, I can feel the arcyne pulse within me...", - "A mystery is revealed before my very eyes. I do not read it, yet I am aware. Gems are the Root's natural arcyne energy, manifest. Perhaps I can use them to better my conceptualization...") + remarks = list( + "Recall that place of white and black, so cold after its season of heat...", + "Time slips away as I devour each pictograph and sigil...", + "Noc is a shrewd God, and his followers' writings are no different...", + "The smell of wet rain fills the room with every turned page...", + "Helical text spans the page like a winding puzzle...", + "Tracing a finger over one rune renders my hand paralyzed, if only for a moment...", + "This page clearly details the benefits of swampweed on one's capacity to conceptualize the arcyne...", + "Conceptualize. Theorize. Feel. Flow. Manifest...", + "Passion. Strength. Power. Victory. The tenets through which we break the chains of reality...", + "Magick is to be kept close, a guarded secret. Noc changed the rules again. I need to catch up...", + "Didn't I just read this page...?", + "A lone illustration of Noc's visage fills this page, his stony gaze boring into my soul...", + "My eyes begin to lid as I finish this chapter. These symbols cast a heavy fog over my mind...", + "Silver. Blade. Mana. Blood. These are the ingredients I'll need to imbibe the very ground with arcyne abilities...", + "Elysium incants speak to me in an extinct tongue immortalized on parchment...", + "My mind wanders and waves. Z's temptations draw close, but I weather through as I finally finish this chapter...", + "I close my eyes for but a moment, and the competing visages of Noc and Z stare into my very soul. I see them blink, and my eyelids open...", + "I am the Root. The Root is me. I must reach it, and the Tree...", + "I feel the arcyne circuits running through my body, empowered with each word I read...", + "Am I reading? Are these words, symbols or inane scribbles? I cannot be sure, yet with each one my eyes glaze over, I can feel the arcyne pulse within me...", + "A mystery is revealed before my very eyes. I do not read it, yet I am aware. Gems are the Root's natural arcyne energy, manifest. Perhaps I can use them to better my conceptualization..." + ) oneuse = FALSE item_weight = 547 GRAMS + /// The mob who owns and originally bound this tome var/owner = null + /// Up to two additional mobs allowed to read this tome var/list/allowed_readers = list() + /// Flat quality bonus stored from a crushed gem, consumed on next read var/stored_gem = FALSE + /// Attunement datum stored from the gem used, applied on next read var/datum/attunement/stored_attunement - var/picked // if the book has had it's style picked or not - var/born_of_rock = FALSE // was a magical stone used to make it instead of a gem? + /// Whether the player has chosen a visual style for this book yet + var/picked = FALSE + /// If TRUE, this tome was made from a magic stone rather than a gem and has a reading penalty + var/born_of_rock = FALSE + /// Multiplier for spell points gained per read. Higher = better book. var/bookquality = 3 + +// ============================================================ +// VISUAL / OPEN STATE +// ============================================================ + +/// The open and closed states share identical mob prop data, so we return +/// one table regardless of open state. /obj/item/book/granter/spellbook/getonmobprop(tag) . = ..() - if(tag) - if(open) - switch(tag) - if("gen") - return list("shrink" = 0.4, - "sx" = -2, - "sy" = -3, - "nx" = 10, - "ny" = -2, - "wx" = 1, - "wy" = -3, - "ex" = 5, - "ey" = -3, - "northabove" = 0, - "southabove" = 1, - "eastabove" = 1, - "westabove" = 0, - "nturn" = 0, - "sturn" = 0, - "wturn" = 0, - "eturn" = 0, - "nflip" = 0, - "sflip" = 0, - "wflip" = 0, - "eflip" = 0) - if("onbelt") - return list("shrink" = 0.3,"sx" = -2,"sy" = -5,"nx" = 4,"ny" = -5,"wx" = 0,"wy" = -5,"ex" = 2,"ey" = -5,"nturn" = 0,"sturn" = 0,"wturn" = 0,"eturn" = 0,"nflip" = 0,"sflip" = 0,"wflip" = 0,"eflip" = 0,"northabove" = 0,"southabove" = 1,"eastabove" = 1,"westabove" = 0) - else - switch(tag) - if("gen") - return list("shrink" = 0.4, - "sx" = -2, - "sy" = -3, - "nx" = 10, - "ny" = -2, - "wx" = 1, - "wy" = -3, - "ex" = 5, - "ey" = -3, - "northabove" = 0, - "southabove" = 1, - "eastabove" = 1, - "westabove" = 0, - "nturn" = 0, - "sturn" = 0, - "wturn" = 0, - "eturn" = 0, - "nflip" = 0, - "sflip" = 0, - "wflip" = 0, - "eflip" = 0) - if("onbelt") - return list("shrink" = 0.3,"sx" = -2,"sy" = -5,"nx" = 4,"ny" = -5,"wx" = 0,"wy" = -5,"ex" = 2,"ey" = -5,"nturn" = 0,"sturn" = 0,"wturn" = 0,"eturn" = 0,"nflip" = 0,"sflip" = 0,"wflip" = 0,"eflip" = 0,"northabove" = 0,"southabove" = 1,"eastabove" = 1,"westabove" = 0) + if(!tag) + return + switch(tag) + if("gen") + return list( + "shrink" = 0.4, + "sx" = -2, "sy" = -3, + "nx" = 10, "ny" = -2, + "wx" = 1, "wy" = -3, + "ex" = 5, "ey" = -3, + "northabove" = 0, "southabove" = 1, + "eastabove" = 1, "westabove" = 0, + "nturn" = 0, "sturn" = 0, "wturn" = 0, "eturn" = 0, + "nflip" = 0, "sflip" = 0, "wflip" = 0, "eflip" = 0 + ) + if("onbelt") + return list( + "shrink" = 0.3, + "sx" = -2, "sy" = -5, + "nx" = 4, "ny" = -5, + "wx" = 0, "wy" = -5, + "ex" = 2, "ey" = -5, + "nturn" = 0, "sturn" = 0, "wturn" = 0, "eturn" = 0, + "nflip" = 0, "sflip" = 0, "wflip" = 0, "eflip" = 0, + "northabove" = 0, "southabove" = 1, + "eastabove" = 1, "westabove" = 0 + ) +/obj/item/book/granter/spellbook/update_icon_state() + . = ..() + icon_state = "[base_icon_state]_[open]" /obj/item/book/granter/spellbook/attack_self(mob/user, list/modifiers) if(!open) @@ -123,21 +110,22 @@ return FALSE /obj/item/book/granter/spellbook/attack_hand_secondary(mob/user, list/modifiers) + //first pick styles if(!picked) var/list/designlist = list("green", "yellow", "brown") var/mob/living/carbon/human/gamer = user if(gamer.job == JOB_COURT_MAGE) designlist = list("steel", "gem", "skin", "mimic") var/the_time = world.time - var/design = input(user, "Select a design.","Spellbook Design") as null|anything in designlist - if(!design) - return - if(world.time > (the_time + 30 SECONDS)) + var/design = input(user, "Select a design.", "Spellbook Design") as null|anything in designlist + if(!design || world.time > (the_time + 30 SECONDS)) return base_icon_state = "spellbook[design]" update_appearance(UPDATE_ICON_STATE) picked = TRUE return + + //now we togge state if(owner == null) owner = user if(!open) @@ -152,9 +140,26 @@ update_appearance(UPDATE_ICON_STATE) user.update_inv_hands() -/obj/item/book/granter/spellbook/update_icon_state() - . = ..() - icon_state = "[base_icon_state]_[open]" +/obj/item/book/granter/spellbook/attack(mob/living/M, mob/living/carbon/human/user, list/modifiers) + if(M.stat == DEAD) + M.visible_message(span_danger("[user] smacks [M]'s lifeless corpse with [src].")) + playsound(src, "punch", 25, TRUE, -1) + return + if(user == M) + to_chat(user, span_warning("I'm already chained to this tome!")) + return + if(!ishuman(M)) + return + M.visible_message( + span_danger("[user] beats [M] over the head with [src]!"), + span_danger("[user] beats [M] over the head with [src]!") + ) + if(allowed_readers.len <= 2 && !allowed_readers.Find(M)) + allowed_readers += M + else + to_chat(user, span_smallnotice("I can't chain this pleboid to my tome...")) + playsound(src, "punch", 25, TRUE, -1) + log_combat(user, M, "attacked", src) /obj/item/book/granter/spellbook/on_reading_start(mob/user) to_chat(user, span_notice("Arcyne mysteries abound in this enigmatic tome, gift of Noc...")) @@ -165,44 +170,58 @@ to_chat(user, span_notice("What was that gibberish? Even for the arcyne it was completely illegible!")) recoil(user) return + user.mind?.has_studied = TRUE var/mob/living/reader = user - var/qualityoflearn = (GET_MOB_ATTRIBUTE_VALUE(reader, STAT_INTELLIGENCE)*2 + (GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/misc/reading)* 5) + (GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane)*5)) + + // Base quality is a blend of INT, reading skill, and arcane skill. + var/qualityoflearn = (GET_MOB_ATTRIBUTE_VALUE(reader, STAT_INTELLIGENCE) * 2 + GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/misc/reading) * 5 + GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) + + // Bonuses from external factors. if(reader.has_status_effect(/datum/status_effect/buff/weed)) to_chat(user, span_smallgreen("Swampweed truly does open one's third eye to the secrets of the arcyne...")) qualityoflearn += 10 - var/obj/effect/decal/cleanable/roguerune/rune = (locate(/obj/effect/decal/cleanable/roguerune) in range(1, user)) + + var/obj/effect/decal/cleanable/ritual_rune/rune = locate(/obj/effect/decal/cleanable/ritual_rune) in range(1, user) if(rune) to_chat(user, span_cultsmall("The rune beneath my feet glows...")) qualityoflearn += rune.spellbonus rune.do_invoke_glow() + if(stored_gem) to_chat(user, span_smallnotice("I can feel the magical energies imbued within the crystalline dust scattered upon my tome resonate with the arcyne...")) qualityoflearn += stored_gem stored_gem = FALSE - if(!isarcyne(user)) - if (gamer != owner) // if you didn't make this book, get fucked. + + // Penalties for untrained readers. + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + if(gamer != owner) qualityoflearn = 1 else - qualityoflearn *= 0.5 - qualityoflearn = min(qualityoflearn, 15) - if (born_of_rock) - // the rock tomes are a *lot* easier to make, so we make them worse by them reducing your chances by 20% + qualityoflearn = min(qualityoflearn * 0.5, 15) + + // Born-of-rock tomes are easier to craft, so they're harder to read. + if(born_of_rock) qualityoflearn *= 0.8 - user.visible_message(span_warning("[user] is filled with arcyne energy! You witness [user.p_their()] body convulse and spark brightly."), \ - span_notice("Noc blesses me. I have been granted knowledge and wisdom beyond my years, this tome's mysteries unveiled one at a time.")) - qualityoflearn = qualityoflearn / 100 - var/spellpoints = (src.bookquality * qualityoflearn) - spellpoints = CEILING(spellpoints, 1) + user.visible_message( + span_warning("[user] is filled with arcyne energy! You witness [user.p_their()] body convulse and spark brightly."), + span_notice("Noc blesses me. I have been granted knowledge and wisdom beyond my years, this tome's mysteries unveiled one at a time.") + ) + + var/spellpoints = CEILING(bookquality * (qualityoflearn / 100), 1) reader.adjust_spell_points(spellpoints) + if(stored_attunement) user.mana_pool?.adjust_attunement(stored_attunement, 0.1 * (spellpoints / 0.2)) - user.log_message("successfully studied their spellbook and gained spellpoints", LOG_ATTACK, color="orange") + + user.log_message("successfully studied their spellbook and gained spellpoints", LOG_ATTACK, color = "orange") onlearned(user) + if(prob(55)) to_chat(user, span_notice("Confounded arcyne mysteries, my notes have gone in circles. I must sleep before I can bring myself to open this damned thing again...")) - user.mind?.add_sleep_experience(/datum/attribute/skill/misc/reading, GET_MOB_ATTRIBUTE_VALUE(reader, STAT_INTELLIGENCE)*10) + user.mind?.add_sleep_experience(/datum/attribute/skill/misc/reading, GET_MOB_ATTRIBUTE_VALUE(reader, STAT_INTELLIGENCE) * 10) + to_chat(user, span_small("My notes include passages I've read before, but don't understand. I must sleep on their meaning...")) /obj/item/book/granter/spellbook/onlearned(mob/user) @@ -213,363 +232,67 @@ var/mob/living/gamer = user gamer.electrocute_act(5, src) -/obj/item/book/granter/spellbook/attack(mob/living/M, mob/living/carbon/human/user, list/modifiers) - if (M.stat != DEAD) - if(user == M) - to_chat(user, span_warning("I'm already chained to this tome!")) - return - if(ishuman(M)) - M.visible_message(span_danger("[user] beats [M] over the head with [src]!"), \ - span_danger("[user] beats [M] over the head with [src]!")) - if(src.allowed_readers.len <= 2 && !src.allowed_readers.Find(user)) - src.allowed_readers += M - else - to_chat(user, span_smallnotice("I can't chain this pleboid to my tome...")) - playsound(src, "punch", 25, TRUE, -1) - log_combat(user, M, "attacked", src) - else - M.visible_message(span_danger("[user] smacks [M]'s lifeless corpse with [src].")) - playsound(src, "punch", 25, TRUE, -1) +/obj/item/book/granter/spellbook/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) + if(!istype(P, /obj/item/gem)) + return ..() + if(stored_gem) + to_chat(user, span_notice("This tome is already coursing with arcyne energies...")) + return + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(user, span_notice("Why am I jamming a gem into a book? I must look like a fool!")) + return + var/obj/item/gem/gem = P + var/crafttime = max(0, 60 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) + if(!do_after(user, crafttime, target = src)) + return + playsound(src, 'sound/magic/glass.ogg', 100, TRUE) + to_chat(user, span_notice("Running my arcyne energy through this crystal, I imbue the tome with my natural essence, attuning it to my state of mind...")) + stored_gem = gem.arcyne_potency + stored_attunement = gem.attuned + qdel(P) -/// Book Types: -/obj/item/book/granter/spellbook/horrible //makeable with magic stones (bad quality ones) +/obj/item/book/granter/spellbook/horrible name = "poorly made tome of the arcyne" desc = "A poorly made book, it barely glows with arcyne and has only small notes on arcyne symbols." bookquality = 1 sellprice = 15 -/obj/item/book/granter/spellbook/mid //decent magic stones and basic crafting materials +/obj/item/book/granter/spellbook/mid name = "beginners tome of the arcyne" - desc = "An obviously handcrafted book, it glows occasionally with arcyne and has a meager amount notes on arcyne symbols." + desc = "An obviously handcrafted book, it glows occasionally with arcyne and has a meager amount of notes on arcyne symbols." bookquality = 2 sellprice = 30 -/obj/item/book/granter/spellbook/apprentice //apprentices get made with obsidian +/obj/item/book/granter/spellbook/apprentice name = "apprentice tome of the arcyne" desc = "A carefully made book, faintly glowing with arcyne and half filled with notes and theory on arcyne symbols." bookquality = 3 sellprice = 75 -/obj/item/book/granter/spellbook/adept //refugee mages &normal loot +/obj/item/book/granter/spellbook/adept name = "adept tome of the arcyne" - desc = "A well-made book, it shines moderately with arcyne light. It has been filled with notes of varying degrees on the arcyne " + desc = "A well-made book, it shines moderately with arcyne light. It has been filled with notes of varying degrees on the arcyne." bookquality = 4 sellprice = 150 -/obj/item/book/granter/spellbook/expert //made from 2nd tier loot item +/obj/item/book/granter/spellbook/expert name = "expert tome of the arcyne" desc = "A well cared for book, shining brightly with arcyne. It has many runes and arcyne symbols scribed within, with detailed notes." bookquality = 6 sellprice = 200 -/obj/item/book/granter/spellbook/master // Court mage & made from 3rd tier loot item +/obj/item/book/granter/spellbook/master name = "masterful tome of the arcyne" desc = "A crackling, glowing book, filled with advanced arcyne runes and symbols that hurt the mind to stare at. A true master of the arcyne has left their mark behind." bookquality = 8 sellprice = 250 -/obj/item/book/granter/spellbook/legendary //max tier lootmade item +/obj/item/book/granter/spellbook/legendary name = "legendary tome of the arcyne" - desc = "An incredible book that gives off glowing arcyne motes, it is filled with runes and arcyne theories that is hard for even masters of arcyne to understand. The arcyne script glows and practically whispers from the page.." + desc = "An incredible book that gives off glowing arcyne motes, it is filled with runes and arcyne theories that is hard for even masters of arcyne to understand. The arcyne script glows and practically whispers from the page..." bookquality = 12 sellprice = 400 -/// Book slapcrafting - -/obj/item/spellbook_unfinished - var/pages_left = 4 - name = "bound scrollpaper" - dropshrink = 0.6 - icon = 'icons/roguetown/items/books.dmi' - icon_state ="basic_book_0" - desc = "Thick scroll paper bound at the spine. It lacks pages." - throw_speed = 1 - throw_range = 5 - w_class = WEIGHT_CLASS_NORMAL //upped to three because books are, y'know, pretty big. (and you could hide them inside eachother recursively forever) - attack_verb = list("bashed", "whacked", "educated") - resistance_flags = FLAMMABLE - drop_sound = 'sound/foley/dropsound/book_drop.ogg' - pickup_sound = 'sound/blank.ogg' - -/obj/item/spellbook_unfinished/pre_arcyne - name = "tome in waiting" - icon_state = "spellbook_unfinished" - desc = "A fully bound tome of scroll paper. It's lacking a certain arcyne energy." - -/obj/item/natural/hide/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) - var/found_table = locate(/obj/structure/table) in (loc) - if(istype(P, /obj/item/paper/scroll)) - if(isturf(loc)&& (found_table)) - var/crafttime = (100 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(src, 'sound/items/book_close.ogg', 100, TRUE) - to_chat(user, span_notice("I add the first few pages to the leather cover...")) - new /obj/item/spellbook_unfinished(loc) - qdel(P) - qdel(src) - else - to_chat(user, "You need to put the [src] on a table to work on it.") - else - return ..() - -/obj/item/spellbook_unfinished/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) - var/found_table = locate(/obj/structure/table) in (loc) - if(istype(P, /obj/item/paper/scroll)) - if(isturf(loc)&& (found_table)) - var/crafttime = (60 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if(pages_left > 0) - playsound(src, 'sound/items/book_page.ogg', 100, TRUE) - pages_left -= 1 - to_chat(user, span_notice("[pages_left+1] left...")) - qdel(P) - else - playsound(src, 'sound/items/book_open.ogg', 100, TRUE) - if(isarcyne(user)) - to_chat(user, span_notice("The book is bound. I must find a catalyst to channel the arcyne into it now.")) - else - to_chat(user, span_notice("I've made an empty book of thick, useless scroll paper. I can't even thumb through it!")) - new /obj/item/spellbook_unfinished/pre_arcyne(loc) - qdel(P) - qdel(src) - else - to_chat(user, "You need to put the [src] on a table to work on it.") - else - return ..() - -/obj/item/spellbook_unfinished/pre_arcyne/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) - var/found_table = locate(/obj/structure/table) in (loc) - if(istype(P, /obj/item/gem/amethyst)) - user.visible_message(span_notice("I run my arcyne energy into the crystal. Its artificial lattices pulse and then fall dormant. It must not be strong enough to make a spellbook with!")) - return - if(istype(P, /obj/item/gem/violet)) - if(isturf(loc)&& (found_table)) - var/crafttime = (100 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if(isarcyne(user)) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] crushes [user.p_their()] [P]! Its powder seeps into the [src]."), \ - span_notice("I run my arcyne energy into the crystal. It shatters and seeps into the cover of the tome! Runes and symbols of an unknowable language cover it's pages now...")) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/expert(loc) - newbook.owner = user - qdel(P) - qdel(src) - else - to_chat(user, span_notice("I press the gem into the cover of the book. What a pretty design this would make!")) - return ..() - else - to_chat(user, "You need to put the [src] on a table to work on it.") - if(istype(P, /obj/item/gem)) - if(isturf(loc)&& (found_table)) - var/crafttime = (100 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if(isarcyne(user)) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] crushes [user.p_their()] [P]! Its powder seeps into the [src]."), \ - span_notice("I run my arcyne energy into the crystal. It shatters and seeps into the cover of the tome! Runes and symbols of an unknowable language cover it's pages now...")) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/adept(loc) - newbook.owner = user - qdel(P) - qdel(src) - else - to_chat(user, span_notice("I press the gem into the cover of the book. What a pretty design this would make!")) - return ..() - else - to_chat(user, "You need to put the [src] on a table to work on it.") - else if (istype(P, /obj/item/natural/stone)) - var/obj/item/natural/stone/the_rock = P - if (the_rock.magic_power) - if(isturf(loc) && (found_table)) - var/crafttime = ((130 - the_rock.magic_power) - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if (isarcyne(user)) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] crushes [user.p_their()] [P]! Its powder seeps into the [src]."), \ - span_notice("I join my arcyne energy with that of the magical stone in my hands, which shudders briefly before dissolving into motes of ash. Runes and symbols of an unknowable language cover its pages now...")) - to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) - if(the_rock.magic_power <=5) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/horrible(loc) - newbook.owner = user - newbook.born_of_rock = TRUE - newbook.desc += " Traces of multicolored stone limn its margins." - qdel(P) - qdel(src) - else if(the_rock.magic_power >5 && the_rock.magic_power <=9) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/mid(loc) - newbook.owner = user - newbook.born_of_rock = TRUE - newbook.desc += " Traces of multicolored stone limn its margins." - qdel(P) - qdel(src) - else if(the_rock.magic_power >=10) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/apprentice(loc) - newbook.owner = user - newbook.born_of_rock = TRUE - newbook.desc += " Traces of multicolored stone limn its margins." - qdel(P) - qdel(src) - else - if (prob(the_rock.magic_power * 5)) // for reference, this is never higher than 15 and usually significantly lower - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] carefully sets down [the_rock] upon [src]. Nothing happens for a moment or three, then suddenly, the glow surrounding the stone becomes as liquid, seeps down and soaks into the tome!"), \ - span_notice("I knew this stone was special! Its colourful magick has soaked into my tome and given me gift of mystery!")) - to_chat(user, span_notice("...what in the world does any of this scribbling possibly mean?")) - if(the_rock.magic_power <=5) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/horrible(loc) - newbook.owner = user - newbook.born_of_rock = TRUE - newbook.desc += " Traces of multicolored stone limn its margins." - qdel(P) - qdel(src) - else if(the_rock.magic_power >5 && the_rock.magic_power <=9) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/mid(loc) - newbook.owner = user - newbook.born_of_rock = TRUE - newbook.desc += " Traces of multicolored stone limn its margins." - qdel(P) - qdel(src) - else if(the_rock.magic_power >=10) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/apprentice(loc) - newbook.owner = user - newbook.born_of_rock = TRUE - newbook.desc += " Traces of multicolored stone limn its margins." - qdel(P) - qdel(src) - else - user.visible_message(span_warning("[user] sets down [the_rock] upon the surface of [src] and watches expectantly. Without warning, the rock violently pops like a squashed gourd!"), \ - span_notice("No! My precious stone! It mustn't have wanted to share its mysteries with me...")) - user.electrocute_act(5, src) - qdel(P) - else - to_chat(user, span_notice("This is a mere rock - it has no arcyne potential. Bah!")) - return ..() - else if (istype(P, /obj/item/natural/melded/t1)) - if(isturf(loc) && (found_table)) - var/crafttime = (100 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if (isarcyne(user)) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] imbues [user.p_their()] [P]! It fuses into the [src]."), \ - span_notice("I join my arcyne energy with that of the [P] in my hands, which shudders briefly before dissolving into motes of energy. Runes and symbols of an unknowable language cover its pages now...")) - to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/adept(loc) - newbook.owner = user - qdel(P) - qdel(src) - else - user.visible_message(span_warning("[user] sets down [P] upon the surface of [src] and watches expectantly. Without warning, the [P] lets out a burst of arcyne energy!"), \ - span_notice("I should have known messing with the arcyne as dangerous!")) - user.electrocute_act(20, src) - qdel(P) - return ..() - else if (istype(P, /obj/item/natural/melded/t2)) - if(isturf(loc) && (found_table)) - var/crafttime = (100 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if (isarcyne(user)) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] crushes [user.p_their()] [P]! Its powder seeps into the [src]."), \ - span_notice("I join my arcyne energy with that of the [P] in my hands, which shudders briefly before dissolving into motes of energy. Runes and symbols of an unknowable language cover its pages now...")) - to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/expert(loc) - newbook.owner = user - qdel(P) - qdel(src) - else - user.visible_message(span_warning("[user] sets down [P] upon the surface of [src] and watches expectantly. Without warning, the [P] violently explodes!"), \ - span_notice("I should have known messing with the arcyne as dangerous!")) - user.electrocute_act(40, src) - qdel(P) - else if (istype(P, /obj/item/natural/melded/t3)) - if(isturf(loc) && (found_table)) - var/crafttime = (100 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if (isarcyne(user)) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] crushes [user.p_their()] [P]! Its powder seeps into the [src]."), \ - span_notice("I join my arcyne energy with that of the [P] in my hands, which shudders briefly before dissolving into motes of energy. Runes and symbols of an unknowable language cover its pages now...")) - to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/master(loc) - newbook.owner = user - qdel(P) - qdel(src) - else - user.visible_message(span_warning("[user] sets down [P] upon the surface of [src] and watches expectantly. Without warning, the [P] violently explodes!"), \ - span_notice("I should have known messing with the arcyne as dangerous!")) - user.electrocute_act(60, src) - qdel(P) - else if (istype(P, /obj/item/natural/melded/t4)) - if(isturf(loc) && (found_table)) - var/crafttime = (100 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - if (isarcyne(user)) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] crushes [user.p_their()] [P]! Its powder seeps into the [src]."), \ - span_notice("I join my arcyne energy with that of the [P] in my hands, which shudders briefly before dissolving into motes of energy. Runes and symbols of an unknowable language cover its pages now...")) - to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) - var/obj/item/book/granter/spellbook/newbook = new /obj/item/book/granter/spellbook/legendary(loc) - newbook.owner = user - qdel(P) - qdel(src) - else - user.visible_message(span_warning("[user] sets down [P] upon the surface of [src] and watches expectantly. Without warning, the [P] violently explodes!"), \ - span_notice("I should have known messing with the arcyne as dangerous!")) - user.electrocute_act(80, src) - qdel(P) - else - return ..() - -// qualityoflearn buff shit - -/obj/item/gem - var/arcyne_potency = 20 - var/datum/attunement/attuned - -/obj/item/gem/yellow - arcyne_potency = 5 - attuned = /datum/attunement/light - -/obj/item/gem/green - arcyne_potency = 7 - attuned = /datum/attunement/earth - -/obj/item/gem/violet - arcyne_potency = 10 - attuned = /datum/attunement/electric - -/obj/item/gem/blue - arcyne_potency = 25 - attuned = /datum/attunement/blood - -/obj/item/gem/diamond - arcyne_potency = 15 - attuned = /datum/attunement/aeromancy - - - -/obj/item/book/granter/spellbook/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) - if(istype(P, /obj/item/gem)) - if(!stored_gem) - if(isarcyne(user)) - var/obj/item/gem/gem = P - var/crafttime = (60 - ((GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(src, 'sound/magic/glass.ogg', 100, TRUE) - to_chat(user, span_notice("Running my arcyne energy through this crystal, I imbue the tome with my natural essence, attuning it to my state of mind...")) - stored_gem = gem.arcyne_potency - stored_attunement = gem.attuned - qdel(P) - else - to_chat(user, span_notice("Why am I jamming a gem into a book? I must look like a fool!")) - else - to_chat(user, span_notice("This tome is already coursing with arcyne energies...")) - else - return ..() - - -// helper proc - - /obj/item/book/granter/spellbook/magician/Initialize() . = ..() var/mob/living/carbon/human/L = loc diff --git a/code/game/objects/items/unfiinshed_spellbook.dm b/code/game/objects/items/unfiinshed_spellbook.dm new file mode 100644 index 00000000000..66267157e8b --- /dev/null +++ b/code/game/objects/items/unfiinshed_spellbook.dm @@ -0,0 +1,170 @@ + +/obj/item/spellbook_unfinished + name = "bound scrollpaper" + dropshrink = 0.6 + icon = 'icons/roguetown/items/books.dmi' + icon_state = "basic_book_0" + desc = "Thick scroll paper bound at the spine. It lacks pages." + throw_speed = 1 + throw_range = 5 + w_class = WEIGHT_CLASS_NORMAL + attack_verb = list("bashed", "whacked", "educated") + resistance_flags = FLAMMABLE + drop_sound = 'sound/foley/dropsound/book_drop.ogg' + pickup_sound = 'sound/blank.ogg' + /// Pages still needed before the binding is complete + var/pages_left = 4 + +/obj/item/spellbook_unfinished/pre_arcyne + name = "tome in waiting" + icon_state = "spellbook_unfinished" + desc = "A fully bound tome of scroll paper. It's lacking a certain arcyne energy." + +/obj/item/spellbook_unfinished/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) + if(!istype(P, /obj/item/paper/scroll)) + return ..() + if(!isturf(loc) || !locate(/obj/structure/table) in loc) + to_chat(user, "You need to put the [src] on a table to work on it.") + return + var/crafttime = max(0, 60 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) + if(!do_after(user, crafttime, target = src)) + return + pages_left-- + if(pages_left > 0) + playsound(src, 'sound/items/book_page.ogg', 100, TRUE) + to_chat(user, span_notice("[pages_left] left...")) + qdel(P) + return + //promote to pre_arcyne. + playsound(src, 'sound/items/book_open.ogg', 100, TRUE) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) + to_chat(user, span_notice("The book is bound. I must find a catalyst to channel the arcyne into it now.")) + else + to_chat(user, span_notice("I've made an empty book of thick, useless scroll paper. I can't even thumb through it!")) + new /obj/item/spellbook_unfinished/pre_arcyne(loc) + qdel(P) + qdel(src) + + +/obj/item/spellbook_unfinished/pre_arcyne/attackby(obj/item/P, mob/living/carbon/human/user, list/modifiers) + var/found_table = locate(/obj/structure/table) in loc + + if(istype(P, /obj/item/gem/amethyst)) + user.visible_message(span_notice("I run my arcyne energy into the crystal. Its artificial lattices pulse and then fall dormant. It must not be strong enough to make a spellbook with!")) + return + + if(isturf(loc) && !found_table) + to_chat(user, "You need to put the [src] on a table to work on it.") + return TRUE + + if(istype(P, /obj/item/gem/violet)) + apply_gem_catalyst(user, P, /obj/item/book/granter/spellbook/expert, found_table) + return + + if(istype(P, /obj/item/gem)) + apply_gem_catalyst(user, P, /obj/item/book/granter/spellbook/adept, found_table) + return + + if(istype(P, /obj/item/natural/stone)) + var/obj/item/natural/stone/the_rock = P + if(!the_rock.magic_power) + to_chat(user, span_notice("This is a mere rock — it has no arcyne potential. Bah!")) + return ..() + apply_stone_catalyst(user, the_rock, found_table) + return + + if(istype(P, /obj/item/natural/melded)) + var/obj/item/natural/melded/meld = P + apply_melded_catalyst(user, P, meld.melded_quality, meld.shock_damage) + return + return ..() + +/// Spawns a finished book, sets owner, and cleans up catalyst + self. +/obj/item/spellbook_unfinished/pre_arcyne/proc/finish_book(mob/user, obj/item/catalyst, book_type, born_of_rock = FALSE, extra_desc = null) + playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) + var/obj/item/book/granter/spellbook/newbook = new book_type(get_turf(loc)) + var/atom/old_loc = loc + newbook.owner = user + if(born_of_rock) + newbook.born_of_rock = TRUE + if(extra_desc) + newbook.desc += extra_desc + qdel(catalyst) + qdel(src) + if(ismob(old_loc)) + var/mob/living/mob = old_loc + mob.put_in_hands(newbook) + +/// Handles gem catalysts. Unskilled readers just get a cosmetic press. +/obj/item/spellbook_unfinished/pre_arcyne/proc/apply_gem_catalyst(mob/user, obj/item/gem/gem, book_type, found_table) + if(!do_after(user, max(0, 100 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5), target = src)) + return + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) + user.visible_message( + span_warning("[user] crushes [user.p_their()] [gem]! Its powder seeps into the [src]."), + span_notice("I run my arcyne energy into the crystal. It shatters and seeps into the cover of the tome! Runes and symbols of an unknowable language cover its pages now...") + ) + finish_book(user, gem, book_type) + else + to_chat(user, span_notice("I press the gem into the cover of the book. What a pretty design this would make!")) + return TRUE + +/// Handles magic stone catalysts. Quality tier is determined by magic_power. +/// Unskilled users have a prob() chance; failure destroys the stone and shocks. +/obj/item/spellbook_unfinished/pre_arcyne/proc/apply_stone_catalyst(mob/living/user, obj/item/natural/stone/the_rock, found_table) + var/crafttime = max(0, (130 - the_rock.magic_power) - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) + if(!do_after(user, crafttime, target = src)) + return + + var/book_type = stone_quality_to_book(the_rock.magic_power) + + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) + user.visible_message( + span_warning("[user] crushes [user.p_their()] [the_rock]! Its powder seeps into the [src]."), + span_notice("I join my arcyne energy with that of the magical stone in my hands, which shudders briefly before dissolving into motes of ash. Runes and symbols of an unknowable language cover its pages now...") + ) + to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) + finish_book(user, the_rock, book_type, born_of_rock = TRUE, extra_desc = " Traces of multicolored stone limn its margins.") + else + // Unskilled: prob chance proportional to magic_power (capped at ~15%). + if(prob(the_rock.magic_power * 5)) + user.visible_message( + span_warning("[user] carefully sets down [the_rock] upon [src]. Nothing happens for a moment or three, then suddenly, the glow surrounding the stone becomes as liquid, seeps down and soaks into the tome!"), + span_notice("I knew this stone was special! Its colourful magick has soaked into my tome and given me gift of mystery!") + ) + to_chat(user, span_notice("...what in the world does any of this scribbling possibly mean?")) + finish_book(user, the_rock, book_type, born_of_rock = TRUE, extra_desc = " Traces of multicolored stone limn its margins.") + else + user.visible_message( + span_warning("[user] sets down [the_rock] upon the surface of [src] and watches expectantly. Without warning, the rock violently pops like a squashed gourd!"), + span_notice("No! My precious stone! It mustn't have wanted to share its mysteries with me...") + ) + user.electrocute_act(5, src) + qdel(the_rock) + +/// Maps a stone's magic_power to the appropriate book subtype. +/obj/item/spellbook_unfinished/pre_arcyne/proc/stone_quality_to_book(magic_power) + if(magic_power >= 10) + return /obj/item/book/granter/spellbook/apprentice + if(magic_power > 5) + return /obj/item/book/granter/spellbook/mid + return /obj/item/book/granter/spellbook/horrible + +/// Handles melded catalysts. Unskilled users take escalating shock damage. +/obj/item/spellbook_unfinished/pre_arcyne/proc/apply_melded_catalyst(mob/living/user, obj/item/P, book_type, shock_damage) + if(!do_after(user, max(0, 100 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5), target = src)) + return + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) + user.visible_message( + span_warning("[user] imbues [user.p_their()] [P]! It fuses into the [src]."), + span_notice("I join my arcyne energy with that of the [P] in my hands, which shudders briefly before dissolving into motes of energy. Runes and symbols of an unknowable language cover its pages now...") + ) + to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) + finish_book(user, P, book_type) + else + user.visible_message( + span_warning("[user] sets down [P] upon the surface of [src] and watches expectantly. Without warning, the [P] violently explodes!"), + span_notice("I should have known messing with the arcyne was dangerous!") + ) + user.electrocute_act(shock_damage, src) + qdel(P) diff --git a/code/game/objects/structures/mage_structures.dm b/code/game/objects/structures/mage_structures.dm index 0911cd09b76..67cdc2c9b3d 100644 --- a/code/game/objects/structures/mage_structures.dm +++ b/code/game/objects/structures/mage_structures.dm @@ -203,7 +203,7 @@ GLOBAL_LIST_EMPTY(mana_fountains) if(last_process + time_between_uses > world.time) to_chat(user, span_notice("The leyline appears to be drained of energy.")) return - if(!isarcyne(user)) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) if(!active) to_chat(user, span_notice("I wave a hand through the circle of rocks. Nothing happens.")) return diff --git a/code/game/objects/structures/mana_pylon.dm b/code/game/objects/structures/mana_pylon.dm index 71f911a3fa9..e69c3e4b0c8 100644 --- a/code/game/objects/structures/mana_pylon.dm +++ b/code/game/objects/structures/mana_pylon.dm @@ -3,9 +3,8 @@ desc = "" icon_state = "pylon" icon = 'icons/roguetown/misc/mana_pylon.dmi' - SET_BASE_PIXEL(0, -32) has_initial_mana_pool = TRUE - plane = GAME_PLANE_UPPER + plane = GAME_PLANE_FOV_HIDDEN layer = ABOVE_MOB_LAYER light_outer_range = MINIMUM_USEFUL_LIGHT_RANGE light_color = COLOR_CYAN @@ -13,9 +12,7 @@ var/obj/structure/mana_pylon/linked_pylon var/datum/beam/created_beam - var/list/transferring_mobs = list() - var/different_z = FALSE /obj/structure/mana_pylon/examine(mob/user) @@ -31,6 +28,17 @@ update_appearance(UPDATE_OVERLAYS) set_light(1.4, 1.4, 0.75, l_color = COLOR_CYAN) +/obj/structure/mana_pylon/AltClick(mob/living/user) + . = ..() + if(!user.client) + return + var/datum/mana_pool/mana_pylon/pool = mana_pool + var/new_threshold = input(user, "Set the minimum mana reserve for this pylon (current: [pool.transfer_threshold]):", "Pylon Threshold", pool.transfer_threshold) as num|null + if(isnull(new_threshold)) + return + pool.transfer_threshold = max(0, new_threshold) + user.balloon_alert(user, "threshold set: [pool.transfer_threshold]") + /obj/structure/mana_pylon/Destroy() if(linked_pylon) unlink_pylon(linked_pylon) @@ -43,11 +51,13 @@ MA.color = COLOR_RED . += MA +/obj/structure/mana_pylon/get_initial_mana_pool_type() + return /datum/mana_pool/mana_pylon + /obj/structure/mana_pylon/attackby(obj/item/I, mob/living/user, list/modifiers) . = ..() if(!istype(I, /obj/item/gem)) return - var/obj/item/gem/gem = I if(!gem.attuned) return @@ -56,27 +66,24 @@ return mana_pool.network_attunement = gem.attuned -/obj/structure/mana_pylon/MouseDrop(obj/structure/over, src_location, over_location, src_control, over_control, params) +/obj/structure/mana_pylon/attack_hand_secondary(mob/user, list/modifiers) . = ..() - if(!isliving(usr)) - return - - if(!istype(over, type)) + if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) return - - link_pylon(over) - -/obj/structure/mana_pylon/get_initial_mana_pool_type() - return /datum/mana_pool/mana_pylon + if(user.client) + drain_mana(user) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN /obj/structure/mana_pylon/proc/link_pylon(obj/structure/mana_pylon/pylon_to_link) if(pylon_to_link.linked_pylon == src) return - if(linked_pylon) unlink_pylon(linked_pylon) - if(pylon_to_link.z == z) + linked_pylon = pylon_to_link + different_z = (pylon_to_link.z != z) + + if(!different_z) created_beam = Beam( pylon_to_link, icon_state = "medbeam", @@ -89,46 +96,33 @@ override_target_pixel_y = 32, ) - if(pylon_to_link.z != z) - different_z = TRUE - else - different_z = FALSE - linked_pylon = pylon_to_link mana_pool.start_transfer(pylon_to_link.mana_pool, TRUE) update_appearance(UPDATE_OVERLAYS) return TRUE -/obj/structure/mana_pylon/proc/unlink_pylon(obj/structure/pylon_to_unlink) +/obj/structure/mana_pylon/proc/unlink_pylon(obj/structure/mana_pylon/pylon_to_unlink) QDEL_NULL(created_beam) linked_pylon = null + different_z = FALSE mana_pool.stop_transfer(pylon_to_unlink.mana_pool) + update_appearance(UPDATE_OVERLAYS) /obj/structure/mana_pylon/proc/drain_mana(mob/living/user) if(mana_pool.network_attunement) var/list/mana_pools = list() - for (var/atom/movable/thing as anything in user.get_all_contents()) - if (!isnull(thing.mana_pool) && thing.mana_pool.network_attunement == mana_pool.network_attunement) + for(var/atom/movable/thing as anything in user.get_all_contents()) + if(!isnull(thing.mana_pool) && thing.mana_pool.network_attunement == mana_pool.network_attunement) mana_pools += thing.mana_pool - if(!length(mana_pools)) return var/datum/beam/transfer_beam = user.Beam(src, icon_state = "drain_life", time = INFINITY, override_target_pixel_y = 32) - var/failed = FALSE - while(!failed) - if(!do_after(user, 3 SECONDS, target = src)) - qdel(transfer_beam) - failed = TRUE - break - if(!user.client) - failed = TRUE - qdel(transfer_beam) + while(TRUE) + if(!do_after(user, 3 SECONDS, target = src) || !user.client) break var/transfer_amount = min(mana_pool.amount, 20) if(!transfer_amount) - failed = TRUE - qdel(transfer_beam) break var/obj/item/mana_battery/mana_crystal/small/focus/foci = user.get_active_held_item() if(istype(foci)) @@ -136,11 +130,25 @@ else mana_pool.transfer_specific_mana(user.mana_pool, transfer_amount, decrement_budget = TRUE) -/obj/structure/mana_pylon/attack_hand_secondary(mob/user, list/modifiers) + qdel(transfer_beam) + +/obj/structure/mana_pylon/MouseDrop_T(mob/living/user, mob/living/over_object) . = ..() - if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + if(!isliving(user) || user != over_object) + return + if(!HAS_TRAIT(user, TRAIT_PYLON_RIDER)) + return + if(get_dist(user, src) > 1) + to_chat(user, span_warning("You're too far from the pylon.")) + return + if(mana_pool.amount < 10) + to_chat(user, span_warning("The pylon doesn't have enough mana to carry you.")) return - if(user.client) - drain_mana(user) - return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + mana_pool.amount -= 10 + user.visible_message( + span_notice("[user] dissolves into the leyline!"), + span_notice("You dissolve into the leyline, riding the flow of mana.") + ) + user.forceMove(get_turf(src)) + new /obj/effect/bloodcult_jaunt/visible/ley(get_turf(user), user, get_turf(linked_pylon), null) diff --git a/code/game/turfs/open/water.dm b/code/game/turfs/open/water.dm index 12b2b616f52..9e98a539a53 100644 --- a/code/game/turfs/open/water.dm +++ b/code/game/turfs/open/water.dm @@ -940,6 +940,7 @@ force_open_above = TRUE /datum/reagent/water/salty + name = "Salt Water" taste_description = "salt" color = "#3e7459" diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm index 533ca439890..40ab4a6b498 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm @@ -63,6 +63,8 @@ continue if(S.spell_flags & SPELL_TEMPORARY) continue + if(S.spell_flags & SPELL_UNETCHABLE) + continue eligible[S.name] = S if(!eligible.len) diff --git a/code/modules/crafting/alchemy/essences.dm b/code/modules/crafting/alchemy/essences.dm index b7af2e2428e..52e145440a1 100644 --- a/code/modules/crafting/alchemy/essences.dm +++ b/code/modules/crafting/alchemy/essences.dm @@ -28,7 +28,7 @@ if(extract_amount >= max_essence) extract_amount = 1 else - if(extract_amount == 1) + if(extract_amount == 1 && max_essence > 10) extract_amount = 0 extract_amount += increments diff --git a/code/modules/crafting/alchemy/ingredients.dm b/code/modules/crafting/alchemy/ingredients.dm index 1e747e63d37..4d7cf36d7af 100644 --- a/code/modules/crafting/alchemy/ingredients.dm +++ b/code/modules/crafting/alchemy/ingredients.dm @@ -93,11 +93,8 @@ /datum/attunement/life = -0.1, /datum/attunement/light = -0.1, ) - -/obj/item/alch/bone/Initialize(mapload) - . = ..() - create_reagents(20) - reagents.add_reagent(/datum/reagent/consumable/nutriment/bone_marrow, 20) + grind_results = list(/datum/reagent/consumable/nutriment/bone_marrow = 20) + indexed = TRUE /obj/item/alch/horn name = "troll horn" diff --git a/code/modules/crafting/quality_of_crafting/arcyne.dm b/code/modules/crafting/quality_of_crafting/arcyne.dm index 9ebea764d31..a5fd8b0f31f 100644 --- a/code/modules/crafting/quality_of_crafting/arcyne.dm +++ b/code/modules/crafting/quality_of_crafting/arcyne.dm @@ -5,164 +5,6 @@ category = "Arcyne" allow_inverse_start = TRUE -/datum/repeatable_crafting_recipe/arcyne/arcana - name = "amethyst transmutation" - output = /obj/item/gem/amethyst - reagent_requirements = list( - /datum/reagent/medicine/manapot = 15 - ) - requirements = list( - /obj/item/natural/stone = TRUE - ) - tool_usage = list( - /obj/item/weapon/knife = list("starts to carve out a rune", "start to carve a rune") - ) - - attacked_atom = /obj/item/natural/stone - starting_atom = /obj/item/weapon/knife - allow_inverse_start = FALSE - subtypes_allowed = TRUE // so you can use any subtype of knife - reagent_subtypes_allowed = TRUE // so normal mana potions can be used as well as weak ones. - -/datum/repeatable_crafting_recipe/arcyne/infernal_feather - name = "infernal feather" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/infernalash = 2, - /obj/item/natural/feather = 1, - ) - output = /obj/item/natural/feather/infernal - attacked_atom = /obj/item/natural/feather - starting_atom = /obj/item/natural/infernalash - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/sending_stone - name = "sending stones" - reagent_requirements = list() - requirements = list( - /obj/item/gem/amethyst = 2, - /obj/item/natural/stone = 2, - /obj/item/natural/melded/t1 = 1, - ) - starting_atom = /obj/item/gem/amethyst - attacked_atom = /obj/item/natural/stone - output = /obj/item/sendingstonesummoner - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/voidlamptern - name = "void lamptern" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/melded/t1 = 1, - /obj/item/natural/obsidian = 1, - /obj/item/flashlight/flare/torch/lantern = 1, - ) - output = /obj/item/flashlight/flare/torch/lantern/voidlamptern - starting_atom = /obj/item/flashlight/flare/torch/lantern - attacked_atom = /obj/item/natural/obsidian - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/nomagicglove - name = "mana binding gloves" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/melded/t3 = 1, - /obj/item/clothing/gloves/leather = 1, - /obj/item/gem = 1, - ) - output = /obj/item/clothing/gloves/nomagic - starting_atom = /obj/item/gem - attacked_atom = /obj/item/clothing/gloves/leather - allow_inverse_start = FALSE - subtypes_allowed = TRUE - craftdiff = 3 - -/datum/repeatable_crafting_recipe/arcyne/temporalhourglass - name = "temporal hourglass" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/wood/plank = 4, - /obj/item/natural/melded/t2 = 1, - /obj/item/natural/glass = 1, - /obj/item/gem = 1, - ) - output = /obj/item/hourglass/temporal - starting_atom = /obj/item/natural/glass - attacked_atom = /obj/item/natural/melded/t2 - subtypes_allowed = TRUE - craftdiff = 3 - -/datum/repeatable_crafting_recipe/arcyne/shimmeringlens - name = "shimmering lens" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/melded/t1 = 1, - /obj/item/natural/leyline = 1, - /obj/item/natural/iridescentscale = 1, - ) - output = /obj/item/clothing/ring/shimmeringlens - starting_atom = /obj/item/natural/iridescentscale - attacked_atom = /obj/item/natural/leyline - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/mimictrinket - name = "mimic trinket" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/melded/t2 = 1, - /obj/item/natural/wood/plank = 2, - ) - output = /obj/item/mimictrinket - starting_atom = /obj/item/natural/wood/plank - attacked_atom = /obj/item/natural/melded/t2 - craftdiff = 3 - -/datum/repeatable_crafting_recipe/arcyne/binding - name = "binding shackles" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/melded/t2 = 1, - /obj/item/ingot/iron = 2, - ) - output = /obj/item/rope/chain/bindingshackles - starting_atom = /obj/item/ingot/iron - attacked_atom = /obj/item/natural/melded/t2 - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/focus - name = "primordial quartz focus" - requirements = list( - /obj/item/natural/melded/t2 = 1, - /obj/item/ingot/gold = 1, - /obj/item/mana_battery/mana_crystal/small - ) - - starting_atom = /obj/item/natural/melded/t2 - attacked_atom = /obj/item/mana_battery/mana_crystal/small - - output = /obj/item/mana_battery/mana_crystal/small/focus - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/sigil - name = "arcyne sigil" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/melded/t3 = 1, - /obj/item/natural/leyline = 1, - ) - output = /obj/item/clothing/ring/arcanesigil - starting_atom = /obj/item/natural/leyline - attacked_atom = /obj/item/natural/melded/t3 - craftdiff = 2 - /datum/repeatable_crafting_recipe/arcyne/mana_chalk name = "mana infused chalk" requirements = list( @@ -190,72 +32,3 @@ output_amount = 1 craft_time = 1 SECONDS subtypes_allowed = TRUE - -/datum/repeatable_crafting_recipe/arcyne/t1_meld - name = "arcanic meld" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/infernalash = 1, - /obj/item/natural/fairydust = 1, - /obj/item/natural/elementalmote = 1, - ) - output = /obj/item/natural/melded/t1 - starting_atom = /obj/item/natural/infernalash - attacked_atom = /obj/item/natural/elementalmote - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/t2_meld - name = "dense arcanic meld" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/hellhoundfang = 1, - /obj/item/natural/iridescentscale = 1, - /obj/item/natural/elementalshard = 1, - ) - output = /obj/item/natural/melded/t2 - starting_atom = /obj/item/natural/hellhoundfang - attacked_atom = /obj/item/natural/elementalshard - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/t3_meld - name = "sorcerous weave" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/moltencore = 1, - /obj/item/natural/heartwoodcore = 1, - /obj/item/natural/elementalfragment = 1, - ) - output = /obj/item/natural/melded/t3 - starting_atom = /obj/item/natural/heartwoodcore - attacked_atom = /obj/item/natural/moltencore - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/t4_meld - name = "magical confluence" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/abyssalflame = 1, - /obj/item/natural/sylvanessence = 1, - /obj/item/natural/elementalrelic = 1 - ) - output = /obj/item/natural/melded/t4 - starting_atom = /obj/item/natural/abyssalflame - attacked_atom = /obj/item/natural/elementalrelic - craftdiff = 2 - -/datum/repeatable_crafting_recipe/arcyne/t5_meld - name = "arcanic abberation" - reagent_requirements = list() - tool_usage = list() - requirements = list( - /obj/item/natural/melded/t4 = 1, - /obj/item/natural/voidstone = 1, - ) - output = /obj/item/natural/melded/t5 - starting_atom = /obj/item/natural/voidstone - attacked_atom = /obj/item/natural/melded/t4 - craftdiff = 2 diff --git a/code/modules/crafting/quality_of_crafting/arcyne_crafting.dm b/code/modules/crafting/quality_of_crafting/arcyne_crafting.dm new file mode 100644 index 00000000000..2f6657eaaab --- /dev/null +++ b/code/modules/crafting/quality_of_crafting/arcyne_crafting.dm @@ -0,0 +1,361 @@ + +/datum/arcyne_crafting_recipe + abstract_type = /datum/arcyne_crafting_recipe + var/name = "unnamed recipe" + /// Unordered list of item typepaths required (one entry per item needed) + var/list/ingredients = list() + /// The typepath of the item produced + var/atom/output + /// Minimum arcane skill level required to invoke + var/required_skill = SKILL_LEVEL_NONE + +/datum/arcyne_crafting_recipe/amethyst_transmutation + name = "amethyst transmutation" + ingredients = list( + /obj/item/natural/stone, + ) + output = /obj/item/gem/amethyst + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/infernal_feather + name = "infernal feather" + ingredients = list( + /obj/item/natural/infernalash, + /obj/item/natural/infernalash, + /obj/item/natural/feather, + ) + output = /obj/item/natural/feather/infernal + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/sending_stones + name = "sending stones" + ingredients = list( + /obj/item/gem/amethyst, + /obj/item/gem/amethyst, + /obj/item/natural/stone, + /obj/item/natural/stone, + /obj/item/natural/melded/t1, + ) + output = /obj/item/sendingstonesummoner + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/void_lamptern + name = "void lamptern" + ingredients = list( + /obj/item/natural/melded/t1, + /obj/item/natural/obsidian, + /obj/item/flashlight/flare/torch/lantern, + ) + output = /obj/item/flashlight/flare/torch/lantern/voidlamptern + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/mana_binding_gloves + name = "mana binding gloves" + ingredients = list( + /obj/item/natural/melded/t3, + /obj/item/clothing/gloves/leather, + /obj/item/gem, + ) + output = /obj/item/clothing/gloves/nomagic + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/temporal_hourglass + name = "temporal hourglass" + ingredients = list( + /obj/item/natural/wood/plank, + /obj/item/natural/wood/plank, + /obj/item/natural/wood/plank, + /obj/item/natural/wood/plank, + /obj/item/natural/melded/t2, + /obj/item/natural/glass, + /obj/item/gem, + ) + output = /obj/item/hourglass/temporal + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/shimmering_lens + name = "shimmering lens" + ingredients = list( + /obj/item/natural/melded/t1, + /obj/item/natural/leyline, + /obj/item/natural/iridescentscale, + ) + output = /obj/item/clothing/ring/shimmeringlens + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/mimic_trinket + name = "mimic trinket" + ingredients = list( + /obj/item/natural/melded/t2, + /obj/item/natural/wood/plank, + /obj/item/natural/wood/plank, + ) + output = /obj/item/mimictrinket + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/binding_shackles + name = "binding shackles" + ingredients = list( + /obj/item/natural/melded/t2, + /obj/item/ingot/iron, + /obj/item/ingot/iron, + ) + output = /obj/item/rope/chain/bindingshackles + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/primordial_focus + name = "primordial quartz focus" + ingredients = list( + /obj/item/natural/melded/t2, + /obj/item/ingot/gold, + /obj/item/mana_battery/mana_crystal/small, + ) + output = /obj/item/mana_battery/mana_crystal/small/focus + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/arcyne_sigil + name = "arcyne sigil" + ingredients = list( + /obj/item/natural/melded/t3, + /obj/item/natural/leyline, + ) + output = /obj/item/clothing/ring/arcanesigil + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/mana_chalk + name = "mana infused chalk" + ingredients = list( + /obj/item/ore/cinnabar, + ) + output = /obj/item/chalk + required_skill = SKILL_LEVEL_NONE + +/datum/arcyne_crafting_recipe/t1_meld + name = "arcanic meld" + ingredients = list( + /obj/item/natural/infernalash, + /obj/item/natural/fairydust, + /obj/item/natural/elementalmote, + ) + output = /obj/item/natural/melded/t1 + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/t2_meld + name = "dense arcanic meld" + ingredients = list( + /obj/item/natural/hellhoundfang, + /obj/item/natural/iridescentscale, + /obj/item/natural/elementalshard, + ) + output = /obj/item/natural/melded/t2 + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/t3_meld + name = "sorcerous weave" + ingredients = list( + /obj/item/natural/moltencore, + /obj/item/natural/heartwoodcore, + /obj/item/natural/elementalfragment, + ) + output = /obj/item/natural/melded/t3 + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/t4_meld + name = "magical confluence" + ingredients = list( + /obj/item/natural/abyssalflame, + /obj/item/natural/sylvanessence, + /obj/item/natural/elementalrelic, + ) + output = /obj/item/natural/melded/t4 + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/t5_meld + name = "arcanic abberation" + ingredients = list( + /obj/item/natural/melded/t4, + /obj/item/natural/voidstone, + ) + output = /obj/item/natural/melded/t5 + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/ley_linker + name = "ley linker" + ingredients = list( + /obj/item/gem/amethyst, + /obj/item/natural/infernalash, + /obj/item/natural/fairydust, + ) + output = /obj/item/pylon_linker + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/infernal_t1_to_t2 + name = "hellhound fang synthesis" + ingredients = list( + /obj/item/natural/infernalash, + /obj/item/natural/infernalash, + /obj/item/natural/infernalash, + ) + output = /obj/item/natural/hellhoundfang + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/infernal_t2_to_t3 + name = "molten core synthesis" + ingredients = list( + /obj/item/natural/hellhoundfang, + /obj/item/natural/hellhoundfang, + /obj/item/natural/hellhoundfang, + ) + output = /obj/item/natural/moltencore + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/infernal_t3_to_t4 + name = "abyssal flame synthesis" + ingredients = list( + /obj/item/natural/moltencore, + /obj/item/natural/moltencore, + /obj/item/natural/moltencore, + ) + output = /obj/item/natural/abyssalflame + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/fairy_t1_to_t2 + name = "iridescent scale synthesis" + ingredients = list( + /obj/item/natural/fairydust, + /obj/item/natural/fairydust, + /obj/item/natural/fairydust, + ) + output = /obj/item/natural/iridescentscale + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/fairy_t2_to_t3 + name = "heartwood core synthesis" + ingredients = list( + /obj/item/natural/iridescentscale, + /obj/item/natural/iridescentscale, + /obj/item/natural/iridescentscale, + ) + output = /obj/item/natural/heartwoodcore + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/fairy_t3_to_t4 + name = "sylvan essence synthesis" + ingredients = list( + /obj/item/natural/heartwoodcore, + /obj/item/natural/heartwoodcore, + /obj/item/natural/heartwoodcore, + ) + output = /obj/item/natural/sylvanessence + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/elemental_t1_to_t2 + name = "elemental shard synthesis" + ingredients = list( + /obj/item/natural/elementalmote, + /obj/item/natural/elementalmote, + /obj/item/natural/elementalmote, + ) + output = /obj/item/natural/elementalshard + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/elemental_t2_to_t3 + name = "elemental fragment synthesis" + ingredients = list( + /obj/item/natural/elementalshard, + /obj/item/natural/elementalshard, + /obj/item/natural/elementalshard, + ) + output = /obj/item/natural/elementalfragment + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/elemental_t3_to_t4 + name = "elemental relic synthesis" + ingredients = list( + /obj/item/natural/elementalfragment, + /obj/item/natural/elementalfragment, + /obj/item/natural/elementalfragment, + ) + output = /obj/item/natural/elementalrelic + required_skill = SKILL_LEVEL_NOVICE + +/datum/arcyne_crafting_recipe/spell_focus + name = "arcyne focus" + ingredients = list( + /obj/item/gem/amethyst, + /obj/item/natural/infernalash, + ) + output = /obj/item/spell_focus + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/arcyne_scroll + name = "arcyne scroll" + ingredients = list( + /obj/item/natural/melded/t1, + /obj/item/ore/cinnabar, + /obj/item/paper, + ) + output = /obj/item/arcyne_spellobject/scroll + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/spellstone_lesser + name = "lesser arcyne spellstone" + ingredients = list( + /obj/item/natural/melded/t1, + /obj/item/gem/amethyst, + /obj/item/natural/stone, + ) + output = /obj/item/arcyne_spellobject/spellstone/lesser + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/spellstone_greater + name = "greater arcyne spellstone" + ingredients = list( + /obj/item/natural/melded/t2, + /obj/item/gem/amethyst, + /obj/item/gem/amethyst, + ) + output = /obj/item/arcyne_spellobject/spellstone/greater + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/spellstone_supreme + name = "supreme arcyne spellstone" + ingredients = list( + /obj/item/natural/melded/t3, + /obj/item/gem/amethyst, + /obj/item/gem/amethyst, + /obj/item/gem/amethyst, + ) + output = /obj/item/arcyne_spellobject/spellstone/supreme + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/arcyne_wand + name = "arcyne wand" + ingredients = list( + /obj/item/natural/melded/t1, + /obj/item/natural/wood/plank, + /obj/item/gem/amethyst, + ) + output = /obj/item/arcyne_spellobject/wand + required_skill = SKILL_LEVEL_APPRENTICE + +/datum/arcyne_crafting_recipe/arcyne_wand_greater + name = "greater arcyne wand" + ingredients = list( + /obj/item/natural/melded/t2, + /obj/item/natural/wood/plank, + /obj/item/gem/amethyst, + /obj/item/gem/amethyst, + ) + output = /obj/item/arcyne_spellobject/wand/greater + required_skill = SKILL_LEVEL_JOURNEYMAN + +/datum/arcyne_crafting_recipe/arcyne_wand_chaotic + name = "chaotic arcyne wand" + ingredients = list( + /obj/item/natural/melded/t2, + /obj/item/natural/wood/plank, + /obj/item/natural/voidstone, + ) + output = /obj/item/arcyne_spellobject/wand/chaotic + required_skill = SKILL_LEVEL_JOURNEYMAN diff --git a/code/modules/crafting/quality_of_crafting/books.dm b/code/modules/crafting/quality_of_crafting/books.dm index 610d208c535..fd701dbbfa5 100644 --- a/code/modules/crafting/quality_of_crafting/books.dm +++ b/code/modules/crafting/quality_of_crafting/books.dm @@ -38,7 +38,7 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) // recipe_info_path, set this on any /atom to redirect hyperlink // lookups to a different typepath's return_recipe_data(). // Example: /obj/item/ore/iron { recipe_info_path = /datum/ore_source/iron } -// Example: /obj/item/hammer { recipe_info_path = /obj/item/recipe_book/blacksmithing } +// Example: /obj/item/hammer { recipe_info_path = /obj/item/recipe_book/blacksmithing } // When null the atom's own return_recipe_data() is called directly. /atom/var/recipe_info_path @@ -53,9 +53,9 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) var/list/types = list() var/open var/can_spawn = TRUE - var/current_category = "All" // Default selected category - var/current_recipe = null // Currently viewed recipe - var/search_query = "" // Current search query + var/current_category = "All" // Default selected category + var/current_recipe = null // Currently viewed recipe + var/search_query = "" // Current search query /obj/item/recipe_book/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) @@ -63,6 +63,11 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) ui = new /datum/tgui(user, src, "RecipeBook", name) ui.open() +/obj/item/recipe_book/ui_state(mob/user) + if(!loc) + return GLOB.always_state + . = ..() + /obj/item/recipe_book/attack_self(mob/user, list/modifiers) . = ..() ui_interact(user) @@ -178,27 +183,45 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) if(!GLOB.mob_source_paths[mob_entry["_path"]]) continue sources += list(list( - "label" = mob_entry["source_label"], - "_path" = mob_entry["_path"], - "name" = mob_entry["name"], - "icon" = mob_entry["icon"], + "label" = mob_entry["source_label"], + "_path" = mob_entry["_path"], + "name" = mob_entry["name"], + "icon" = mob_entry["icon"], "icon_state" = mob_entry["icon_state"], )) if(!length(sources)) continue var/list/page = list( - "type" = "obtained_from", - "name" = initial(src_path.name), - "category" = "Sources", + "type" = "obtained_from", + "name" = initial(src_path.name), + "category" = "Sources", "_output_path" = "[src_path]", - "output_name" = initial(src_path.name), - "output_icon" = "[initial(src_path.icon)]", + "output_name" = initial(src_path.name), + "output_icon" = "[initial(src_path.icon)]", "output_state" = "[initial(src_path.icon_state)]", - "sources" = sources, + "sources" = sources, ) GLOB.recipe_data_cache["[src_path]"] = page linked += list(page) + for(var/key in GLOB.indexed_item_paths) + if(visited[key]) + continue // already picked up by get_source_page_data or obtained_from + var/atom/item_path = text2path(key) + if(!item_path) + continue + var/list/existing = get_cached_recipe_data(item_path) + if(existing) + continue + var/obj/item/inst = new item_path() + var/list/entry = inst.return_recipe_data() + qdel(inst) + if(!entry) + continue + GLOB.recipe_data_cache[key] = entry + linked += list(entry) + + GLOB.linked_recipe_cache["[book_type]"] = linked return linked @@ -206,14 +229,14 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) var/list/drops = GLOB.obtained_from_reverse["[src_path]"] if(!length(drops)) return null return list( - "type" = "source_page", - "name" = initial(src_path.name), - "category" = "Sources", + "type" = "source_page", + "name" = initial(src_path.name), + "category" = "Sources", "_output_path" = "[src_path]", - "output_name" = initial(src_path.name), - "output_icon" = "[initial(src_path.icon)]", + "output_name" = initial(src_path.name), + "output_icon" = "[initial(src_path.icon)]", "output_state" = "[initial(src_path.icon_state)]", - "drops" = drops, + "drops" = drops, ) /obj/item/recipe_book/proc/get_mob_source_page_data(atom/mob_path) @@ -227,26 +250,26 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) if(!drop_path) continue drops += list(list( - "name" = initial(drop_path.name), - "icon" = "[initial(drop_path.icon)]", - "icon_state" = "[initial(drop_path.icon_state)]", - "_path" = "[drop_path]", + "name" = initial(drop_path.name), + "icon" = "[initial(drop_path.icon)]", + "icon_state" = "[initial(drop_path.icon_state)]", + "_path" = "[drop_path]", "source_label" = entry["source_label"], - "amount" = entry["amount"], + "amount" = entry["amount"], )) if(!length(drops)) return null return list( - "type" = "source_page", - "name" = initial(mob_path.name), - "category" = "Sources", + "type" = "source_page", + "name" = initial(mob_path.name), + "category" = "Sources", "_output_path" = "[mob_path]", - "output_name" = initial(mob_path.name), - "output_icon" = "[initial(mob_path.icon)]", + "output_name" = initial(mob_path.name), + "output_icon" = "[initial(mob_path.icon)]", "output_state" = "[initial(mob_path.icon_state)]", - "drops" = drops, + "drops" = drops, ) /// Returns cached recipe data for a typepath, computing and caching it if needed. @@ -520,6 +543,7 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) /datum/book_entry/grimoire, /datum/book_entry/attunement, /datum/book_entry/mana_sources, + /datum/arcyne_crafting_recipe, /datum/repeatable_crafting_recipe/arcyne, /datum/blueprint_recipe/arcyne, /datum/container_craft/cooking/arcyne, @@ -537,6 +561,8 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) /datum/book_entry/gnome_homunculus, /datum/book_entry/essence_crafting, /datum/alch_cauldron_recipe, + /datum/chemical_reaction, + /datum/distillation_recipe, /datum/essence_combination, /datum/natural_precursor, /datum/infusion_recipe, @@ -552,7 +578,8 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) name = "Survival" can_spawn = FALSE types = list( - /datum/repeatable_crafting_recipe/survival) + /datum/repeatable_crafting_recipe/survival + ) /obj/item/recipe_book/agriculture name = "The Farmers Almanac: Principles of Growth and Harvest" diff --git a/code/modules/crafting/quality_of_crafting/recipe_generation/recipes.dm b/code/modules/crafting/quality_of_crafting/recipe_generation/recipes.dm index 0275c2c5ec1..fcc2834218f 100644 --- a/code/modules/crafting/quality_of_crafting/recipe_generation/recipes.dm +++ b/code/modules/crafting/quality_of_crafting/recipe_generation/recipes.dm @@ -20,16 +20,16 @@ return out /datum/proc/tools_list(list/L, subtypes_ok = FALSE) - var/list/out = list() - for(var/atom/path as anything in L) - out += list(list( - "name" = initial(path.name), - "icon" = "[initial(path.icon)]", - "icon_state" = "[initial(path.icon_state)]", - "any" = subtypes_ok, - "_path" = "[path]", - )) - return out + var/list/out = list() + for(var/atom/path as anything in L) + out += list(list( + "name" = initial(path.name), + "icon" = "[initial(path.icon)]", + "icon_state" = "[initial(path.icon_state)]", + "any" = subtypes_ok, + "_path" = "[path]", + )) + return out /// Converts a reagent typepath=amount assoc list /datum/proc/reagents_list(list/L) @@ -463,7 +463,7 @@ yields += list(list("name" = path.name, "amount" = essence_yields[path])) yield_names += path.name data["yields"] = yields - data["yield_names"] = yield_names // flat list for essence picker indexing + data["yield_names"] = yield_names // flat list for essence picker indexing data["search_data"] = yield_names.Join(",") var/list/splits = list() @@ -478,7 +478,7 @@ // Use the first path as _output_path for the main lookup key. if(length(split_paths)) data["_output_path"] = split_paths[1] - data["_extra_output_paths"] = split_paths // all paths for multi-registration + data["_extra_output_paths"] = split_paths // all paths for multi-registration return data @@ -545,7 +545,7 @@ if(S.skill_used && S.skill_min) step_e["skill_name"] = initial(S.skill_used.name) step_e["skill_min"] = SSskills.level_names[FLOOR(S.skill_min * 0.1, 1)] - step_e["skill_median"] = SSskills.level_names[S.skill_median] + step_e["skill_median"] = SSskills.level_names[FLOOR(S.skill_median * 0.1, 1)] if(length(S.chems_needed)) step_e["chems"] = S.get_chem_string() @@ -714,11 +714,11 @@ var/likelihood if(w <= 0) likelihood = "Very Unlikely" - else if(w < 5) likelihood = "Unlikely" + else if(w < 5) likelihood = "Unlikely" else if(w < 10) likelihood = "Less Likely" else if(w < 15) likelihood = "Likely" else if(w < 25) likelihood = "More Likely" - else likelihood = "Very Likely" + else likelihood = "Very Likely" out += list(list("name" = initial(path.name), "likelihood" = likelihood)) return out @@ -790,3 +790,109 @@ data["hydration_req"] = hydration_req return data + +/datum/chemical_reaction/return_recipe_data() + var/list/data = list() + data["type"] = "chemical_reaction" + data["name"] = name + data["category"] = "Chemistry" + + // Results: list(reagent_type_path = unit_amount) + var/list/result_list = list() + for(var/reagent_type in results) + result_list += list(list( + "name" = initial(reagent_type:name), + "amount" = results[reagent_type] + )) + data["results"] = result_list + + // Required reagents + var/list/req_list = list() + for(var/reagent_type in required_reagents) + req_list += list(list( + "name" = initial(reagent_type:name), + "amount" = required_reagents[reagent_type] + )) + data["required_reagents"] = req_list + + // Catalysts (present but not consumed) + var/list/cat_list = list() + for(var/reagent_type in required_catalysts) + cat_list += list(list( + "name" = initial(reagent_type:name), + "amount" = required_catalysts[reagent_type] + )) + data["required_catalysts"] = cat_list + + data["required_temp"] = required_temp // 0 = no temp needed + data["is_cold_recipe"] = is_cold_recipe // 1 = must be BELOW temp + data["mob_react"] = mob_react + + if(required_container) + data["required_container"] = initial(required_container:name) + if(mix_message) + data["mix_message"] = mix_message + + return data + +/datum/distillation_recipe/return_recipe_data() + var/list/data = list() + data["type"] = "distillation" + data["name"] = name + data["category"] = "Distillation" + + // Primary input that gets vaporized + data["distilled_reagent_name"] = initial(distilled_reagent:name) + data["required_temp"] = required_temp + data["consume_reagents"] = consume_reagents + + // Optional co-reagents that must be present + var/list/req_list = list() + for(var/reagent_type in required_reagents) + req_list += list(list( + "name" = initial(reagent_type:name), + "amount" = required_reagents[reagent_type] + )) + data["required_reagents"] = req_list + + // Output reagents: list(reagent_type = amount_per_unit_distilled) + var/list/result_list = list() + for(var/reagent_type in results) + result_list += list(list( + "name" = initial(reagent_type:name), + "amount" = results[reagent_type] + )) + data["results"] = result_list + + if(distill_message) + data["distill_message"] = distill_message + + return data + +/datum/arcyne_crafting_recipe/return_recipe_data() + var/list/data = list() + data["type"] = "arcyne_crafting" + data["name"] = name ? name : initial(output:name) // fallback to output name + data["category"] = "Arcyne Crafting" + + // Ingredients — unordered item typepaths + var/list/ing_list = list() + for(var/item_type in ingredients) + ing_list += list(list( + "name" = initial(item_type:name), + "icon" = "[initial(item_type:icon)]", + "icon_state" = "[initial(item_type:icon_state)]", + "_path" = "[item_type]" + )) + data["ingredients"] = ing_list + + // Output item + data["output_name"] = initial(output:name) + data["output_icon"] = "[initial(output:icon)]" + data["output_state"] = "[initial(output:icon_state)]" + data["_output_path"] = "[output]" + + // Skill gate — expose the raw numeric level so the UI can label it + data["required_skill"] = required_skill + + return data diff --git a/code/modules/crafting/quality_of_crafting/recipe_generation/reverse_index.dm b/code/modules/crafting/quality_of_crafting/recipe_generation/reverse_index.dm index a6fe8c81194..48e84794364 100644 --- a/code/modules/crafting/quality_of_crafting/recipe_generation/reverse_index.dm +++ b/code/modules/crafting/quality_of_crafting/recipe_generation/reverse_index.dm @@ -8,6 +8,7 @@ GLOBAL_LIST_EMPTY(obtained_from_reverse) GLOBAL_VAR_INIT(obtained_from_built, FALSE) GLOBAL_LIST_EMPTY(mob_source_paths) // list of "[mob_typepath]" keys that have butcher drops GLOBAL_LIST_EMPTY(chimeric_mob_sources) // "[chimeric_table_path]" -> list of mob icon entries +GLOBAL_LIST_EMPTY(indexed_item_paths) /proc/build_obtained_from_reverse() var/list/list = list() @@ -16,6 +17,8 @@ GLOBAL_LIST_EMPTY(chimeric_mob_sources) // "[chimeric_table_path]" -> list of mo for(var/obj/item/item_type as anything in subtypesof(/obj/item)) if(IS_ABSTRACT(item_type)) continue + if(initial(item_type.indexed)) + GLOB.indexed_item_paths["[item_type]"] = TRUE if(!(initial(item_type.item_flags) & OBTAINED_DATA)) continue var/obj/item/new_item = new item_type() diff --git a/code/modules/food_and_drinks/food/snacks.dm b/code/modules/food_and_drinks/food/snacks.dm index 722a83a307f..e5c5450f8f3 100644 --- a/code/modules/food_and_drinks/food/snacks.dm +++ b/code/modules/food_and_drinks/food/snacks.dm @@ -41,6 +41,7 @@ All foods are distributed among various categories. Use common sense. possible_item_intents = list(/datum/intent/food) foodtype = GRAIN list_reagents = list() + indexed = TRUE var/nutrition = 1 var/vitamin = 0.25 w_class = WEIGHT_CLASS_SMALL @@ -48,7 +49,7 @@ All foods are distributed among various categories. Use common sense. var/bitesize = 3 // how many times you need to bite to consume it fully var/bitecount = 0 var/trash = null - var/slice_path // for sliceable food. path of the item resulting from the slicing + var/slice_path // for sliceable food. path of the item resulting from the slicing var/slice_skill var/slice_bclass = BCLASS_CUT var/slices_num @@ -60,11 +61,11 @@ All foods are distributed among various categories. Use common sense. var/dunkable = FALSE // for dunkable food, make true var/dunk_amount = 10 // how much reagent is transferred per dunk var/filling_color = "#FFFFFF" //color to use when added to custom food. - var/custom_food_type = null //for food customizing. path of the custom food to create - var/junkiness = 0 //for junk food. used to lower human satiety. + var/custom_food_type = null //for food customizing. path of the custom food to create + var/junkiness = 0 //for junk food. used to lower human satiety. var/list/bonus_reagents //the amount of reagents (usually nutriment and vitamin) added to crafted/cooked snacks, on top of the ingredients reagents. var/customfoodfilling = 1 // whether it can be used as filling in custom food - var/list/tastes // for example list("crisps" = 2, "salt" = 1) + var/list/tastes // for example list("crisps" = 2, "salt" = 1) var/naturalist = FALSE var/cooking = 0 @@ -114,30 +115,31 @@ All foods are distributed among various categories. Use common sense. return ..() /obj/item/reagent_containers/food/snacks/return_recipe_data() - var/has_mill = !isnull(mill_result) + var/has_mill = !isnull(mill_result) var/has_grind = length(grind_results) var/has_juice = length(juice_results) var/has_slice = !isnull(slice_path) + var/has_list_reagents = length(list_reagents) var/list/milled_from_paths = GLOB.snack_mill_reverse[type] var/list/sliced_from_paths = GLOB.snack_slice_reverse[type] - if(!has_mill && !has_grind && !has_juice && !has_slice && !length(milled_from_paths) && !length(sliced_from_paths)) + if(!has_mill && !has_grind && !has_list_reagents && !has_juice && !has_slice && !length(milled_from_paths) && !length(sliced_from_paths)) return null var/list/data = list() - data["type"] = "snack_processing" - data["name"] = name - data["category"] = "Processing" + data["type"] = "snack_processing" + data["name"] = name + data["category"] = "Processing" data["_output_path"] = "[type]" - data["output_name"] = name - data["output_icon"] = "[icon]" + data["output_name"] = name + data["output_icon"] = "[icon]" data["output_state"] = "[icon_state]" if(has_mill) - data["mill_name"] = initial(mill_result.name) - data["mill_icon"] = "[initial(mill_result.icon)]" + data["mill_name"] = initial(mill_result.name) + data["mill_icon"] = "[initial(mill_result.icon)]" data["mill_state"] = "[initial(mill_result.icon_state)]" - data["mill_path"] = "[mill_result]" + data["mill_path"] = "[mill_result]" if(has_grind) var/list/grind = list() @@ -145,19 +147,20 @@ All foods are distributed among various categories. Use common sense. grind += list(list("name" = initial(path.name), "amount" = grind_results[path])) data["grind_results"] = grind - if(has_juice) + if(has_juice || has_list_reagents) var/list/juice = list() - for(var/datum/reagent/path as anything in juice_results) - juice += list(list("name" = initial(path.name), "amount" = juice_results[path])) + var/list/combined_path = juice_results + list_reagents + for(var/datum/reagent/path as anything in combined_path) + juice += list(list("name" = initial(path.name), "amount" = combined_path[path])) data["juice_results"] = juice if(has_slice) var/atom/slicer = slice_path - data["slice_name"] = initial(slicer.name) - data["slice_icon"] = "[initial(slicer.icon)]" + data["slice_name"] = initial(slicer.name) + data["slice_icon"] = "[initial(slicer.icon)]" data["slice_state"] = "[initial(slicer.icon_state)]" - data["slice_path"] = "[slice_path]" - data["slice_num"] = slices_num + data["slice_path"] = "[slice_path]" + data["slice_num"] = slices_num if(slice_skill) data["slice_skill"] = initial(slicer.name) @@ -165,10 +168,10 @@ All foods are distributed among various categories. Use common sense. var/list/sliced_from = list() for(var/atom/src_path as anything in sliced_from_paths) sliced_from += list(list( - "name" = initial(src_path.name), - "icon" = "[initial(src_path.icon)]", + "name" = initial(src_path.name), + "icon" = "[initial(src_path.icon)]", "icon_state" = "[initial(src_path.icon_state)]", - "_path" = "[src_path]", + "_path" = "[src_path]", )) data["sliced_from"] = sliced_from @@ -176,10 +179,10 @@ All foods are distributed among various categories. Use common sense. var/list/milled_from = list() for(var/atom/src_path as anything in milled_from_paths) milled_from += list(list( - "name" = initial(src_path.name), - "icon" = "[initial(src_path.icon)]", + "name" = initial(src_path.name), + "icon" = "[initial(src_path.icon)]", "icon_state" = "[initial(src_path.icon_state)]", - "_path" = "[src_path]", + "_path" = "[src_path]", )) data["milled_from"] = milled_from @@ -190,10 +193,10 @@ All foods are distributed among various categories. Use common sense. var/label = entry[1] var/atom/src_path = entry[2] sources += list(list( - "label" = label, - "_path" = "[src_path]", - "name" = initial(src_path.name), - "icon" = "[initial(src_path.icon)]", + "label" = label, + "_path" = "[src_path]", + "name" = initial(src_path.name), + "icon" = "[initial(src_path.icon)]", "icon_state" = "[initial(src_path.icon_state)]", )) data["sources"] = sources @@ -233,7 +236,7 @@ All foods are distributed among various categories. Use common sense. if(rotprocess) var/turf/open/T = get_turf(src) var/temp_modifier = 1.0 - var/turf_temp = T?.return_temperature() + var/turf_temp = T?.return_temperature() var/obj/structure/closet/dirthole/dirtgrave = recursive_loc_check(src, /obj/structure/closet/dirthole) var/obj/structure/closet/crate/chest/chest = recursive_loc_check(src, /obj/structure/closet/crate/chest) diff --git a/code/modules/hydroponics/grown/towercap.dm b/code/modules/hydroponics/grown/towercap.dm index e20ae656aad..7188ead5853 100644 --- a/code/modules/hydroponics/grown/towercap.dm +++ b/code/modules/hydroponics/grown/towercap.dm @@ -11,20 +11,15 @@ attack_verb = list("bashed", "battered", "bludgeoned", "whacked") grid_width = 64 grid_height = 32 + grind_results = list(/datum/reagent/tree_sap = 15) + indexed = TRUE var/plank_name = "wooden planks" - var/list/sap = list(/datum/reagent/tree_sap = 15) var/static/list/accepted = typecacheof(list(/*/obj/item/reagent_containers/food/snacks/grown/tobacco, /obj/item/reagent_containers/food/snacks/grown/tea, /obj/item/reagent_containers/food/snacks/grown/ambrosia/vulgaris, /obj/item/reagent_containers/food/snacks/grown/ambrosia/deus, /obj/item/reagent_containers/food/snacks/produce/grain/wheat*/)) -/obj/item/grown/log/Initialize(newloc, obj/item/neuFarm/seed/new_seed) - . = ..() - if(sap) - create_reagents(20) - reagents.add_reagent_list(sap) - /obj/item/grown/log/proc/CheckAccepted(obj/item/I) return is_type_in_typecache(I, accepted) diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index 541f1ee91b2..0b6f9677848 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -229,6 +229,9 @@ var/attribute_sheet_child var/attribute_sheet_adult + ///this is our book path given on middle clicking ui + var/obj/item/recipe_book/book_type = /obj/item/recipe_book/survival + /datum/job/New() . = ..() if(give_bank_account) diff --git a/code/modules/jobs/job_types/apprentices/bapprentice.dm b/code/modules/jobs/job_types/apprentices/bapprentice.dm index 7c20c398778..97c810e9b05 100644 --- a/code/modules/jobs/job_types/apprentices/bapprentice.dm +++ b/code/modules/jobs/job_types/apprentices/bapprentice.dm @@ -44,6 +44,7 @@ ) skill_multipliers = list(/datum/attribute/skill/craft/blacksmithing = 1.25, /datum/attribute/skill/craft/armorsmithing = 1.25, /datum/attribute/skill/craft/weaponsmithing = 1.25) + book_type = /obj/item/recipe_book/blacksmithing /datum/outfit/bapprentice name = JOB_SMITHY_APP diff --git a/code/modules/jobs/job_types/apprentices/clinicapprentice.dm b/code/modules/jobs/job_types/apprentices/clinicapprentice.dm index b08b7e85e53..40425a6eb77 100644 --- a/code/modules/jobs/job_types/apprentices/clinicapprentice.dm +++ b/code/modules/jobs/job_types/apprentices/clinicapprentice.dm @@ -70,7 +70,7 @@ exp_types_granted = list(EXP_TYPE_MEDICAL) skill_multipliers = list(/datum/attribute/skill/misc/medicine = 1.25, /datum/attribute/skill/craft/alchemy = 1.25) - + book_type = /obj/item/recipe_book/medical /datum/outfit/clinicapprentice name = JOB_CLINIC_APP diff --git a/code/modules/jobs/job_types/apprentices/servant.dm b/code/modules/jobs/job_types/apprentices/servant.dm index 35818cc7573..359d4126dd7 100644 --- a/code/modules/jobs/job_types/apprentices/servant.dm +++ b/code/modules/jobs/job_types/apprentices/servant.dm @@ -67,6 +67,7 @@ mind_traits = list( TRAIT_ROYALSERVANT ) + book_type = /obj/item/recipe_book/cooking /datum/outfit/servant name = JOB_SERVANT diff --git a/code/modules/jobs/job_types/apprentices/wapprentice.dm b/code/modules/jobs/job_types/apprentices/wapprentice.dm index 74423245274..9b576060ce3 100644 --- a/code/modules/jobs/job_types/apprentices/wapprentice.dm +++ b/code/modules/jobs/job_types/apprentices/wapprentice.dm @@ -66,6 +66,7 @@ attribute_sheet_adult = /datum/attribute_holder/sheet/job/mageapprentice/adult skill_multipliers = list(/datum/attribute/skill/magic/arcane = 1.25) + book_type = /obj/item/recipe_book/arcyne /datum/job/mageapprentice/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() diff --git a/code/modules/jobs/job_types/nobility/courtphys.dm b/code/modules/jobs/job_types/nobility/courtphys.dm index ff6b757d8cd..2abe549d410 100644 --- a/code/modules/jobs/job_types/nobility/courtphys.dm +++ b/code/modules/jobs/job_types/nobility/courtphys.dm @@ -61,6 +61,7 @@ TRAIT_DEADNOSE, TRAIT_LEGENDARY_ALCHEMIST ) + book_type = /obj/item/recipe_book/medical /datum/job/courtphys/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() diff --git a/code/modules/jobs/job_types/nobility/magician.dm b/code/modules/jobs/job_types/nobility/magician.dm index b981eb2ec35..f00a8894ab6 100644 --- a/code/modules/jobs/job_types/nobility/magician.dm +++ b/code/modules/jobs/job_types/nobility/magician.dm @@ -66,6 +66,7 @@ job_bitflag = BITFLAG_ROYALTY max_apprentices = 2 honorary = "Archmage" + book_type = /obj/item/recipe_book/arcyne spells = list( /datum/action/cooldown/spell/aoe/knock, diff --git a/code/modules/jobs/job_types/peasants/butcher.dm b/code/modules/jobs/job_types/peasants/butcher.dm index 085236a52cc..675b5340879 100644 --- a/code/modules/jobs/job_types/peasants/butcher.dm +++ b/code/modules/jobs/job_types/peasants/butcher.dm @@ -40,6 +40,7 @@ traits = list( TRAIT_STEELHEARTED ) + book_type = /obj/item/recipe_book/agriculture /datum/outfit/beastmaster name = JOB_BUTCHER diff --git a/code/modules/jobs/job_types/peasants/butler.dm b/code/modules/jobs/job_types/peasants/butler.dm index 11b4a85fa2a..96b6e40f4f1 100644 --- a/code/modules/jobs/job_types/peasants/butler.dm +++ b/code/modules/jobs/job_types/peasants/butler.dm @@ -57,6 +57,7 @@ TRAIT_KNOW_KEEP_DOORS, TRAIT_ROYALSERVANT ) + book_type = /obj/item/recipe_book/cooking /datum/outfit/butler name = JOB_BUTLER diff --git a/code/modules/jobs/job_types/peasants/cook.dm b/code/modules/jobs/job_types/peasants/cook.dm index 1908ddf7092..dc5efde62c7 100644 --- a/code/modules/jobs/job_types/peasants/cook.dm +++ b/code/modules/jobs/job_types/peasants/cook.dm @@ -52,7 +52,7 @@ attribute_sheet = /datum/attribute_holder/sheet/job/cook attribute_sheet_old = /datum/attribute_holder/sheet/job/cook/old - + book_type = /obj/item/recipe_book/cooking /datum/outfit/cook name = JOB_COOK diff --git a/code/modules/jobs/job_types/peasants/fisher.dm b/code/modules/jobs/job_types/peasants/fisher.dm index 34801b9b3f9..e396a1a53c8 100644 --- a/code/modules/jobs/job_types/peasants/fisher.dm +++ b/code/modules/jobs/job_types/peasants/fisher.dm @@ -55,6 +55,7 @@ job_bitflag = BITFLAG_CONSTRUCTOR attribute_sheet = /datum/attribute_holder/sheet/job/fisher attribute_sheet_old = /datum/attribute_holder/sheet/job/fisher/old + book_type = /obj/item/recipe_book/survival /datum/outfit/fisher name = JOB_FISHER diff --git a/code/modules/jobs/job_types/peasants/soilson.dm b/code/modules/jobs/job_types/peasants/soilson.dm index 76191f45f53..9a6623ef482 100644 --- a/code/modules/jobs/job_types/peasants/soilson.dm +++ b/code/modules/jobs/job_types/peasants/soilson.dm @@ -55,6 +55,7 @@ TRAIT_DEADNOSE, TRAIT_SEEDKNOW ) + book_type = /obj/item/recipe_book/agriculture /datum/outfit/farmer/map_override(mob/living/carbon/human/H) if(SSmapping.config.map_name != "Voyage") diff --git a/code/modules/jobs/job_types/serfs/apothecary.dm b/code/modules/jobs/job_types/serfs/apothecary.dm index 30491f670bd..f64961613e4 100644 --- a/code/modules/jobs/job_types/serfs/apothecary.dm +++ b/code/modules/jobs/job_types/serfs/apothecary.dm @@ -74,7 +74,7 @@ exp_requirements = list( EXP_TYPE_LIVING = 600 ) - + book_type = /obj/item/recipe_book/alchemy /datum/job/apothecary/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() diff --git a/code/modules/jobs/job_types/serfs/artificer.dm b/code/modules/jobs/job_types/serfs/artificer.dm index e0d5ff975e7..153a67c1f29 100644 --- a/code/modules/jobs/job_types/serfs/artificer.dm +++ b/code/modules/jobs/job_types/serfs/artificer.dm @@ -71,6 +71,7 @@ attribute_sheet = /datum/attribute_holder/sheet/job/artificer attribute_sheet_old = /datum/attribute_holder/sheet/job/artificer/old + book_type = /obj/item/recipe_book/engineering /datum/outfit/artificer name = JOB_ARTIFICER diff --git a/code/modules/jobs/job_types/serfs/blacksmith.dm b/code/modules/jobs/job_types/serfs/blacksmith.dm index 5436d382ffd..1106ed2b816 100644 --- a/code/modules/jobs/job_types/serfs/blacksmith.dm +++ b/code/modules/jobs/job_types/serfs/blacksmith.dm @@ -56,7 +56,7 @@ attribute_sheet = /datum/attribute_holder/sheet/job/blacksmith attribute_sheet_old = /datum/attribute_holder/sheet/job/blacksmith/old - + book_type = /obj/item/recipe_book/blacksmithing /datum/outfit/blacksmith name = JOB_BLACKSMITH diff --git a/code/modules/jobs/job_types/serfs/carpenter.dm b/code/modules/jobs/job_types/serfs/carpenter.dm index 9d8a5f59c37..44cfd28d003 100644 --- a/code/modules/jobs/job_types/serfs/carpenter.dm +++ b/code/modules/jobs/job_types/serfs/carpenter.dm @@ -45,7 +45,7 @@ attribute_sheet = /datum/attribute_holder/sheet/job/carpenter traits = list() - + book_type = /obj/item/recipe_book/carpentry /datum/outfit/carpenter name = JOB_CARPENTER diff --git a/code/modules/jobs/job_types/serfs/feldsher.dm b/code/modules/jobs/job_types/serfs/feldsher.dm index f3c00b1dce2..460411fe8de 100644 --- a/code/modules/jobs/job_types/serfs/feldsher.dm +++ b/code/modules/jobs/job_types/serfs/feldsher.dm @@ -85,6 +85,7 @@ EXP_TYPE_MEDICAL = 300 ) + book_type = /obj/item/recipe_book/medical /datum/outfit/feldsher shoes = /obj/item/clothing/shoes/shortboots diff --git a/code/modules/jobs/job_types/serfs/innkeep.dm b/code/modules/jobs/job_types/serfs/innkeep.dm index 68d57c40ea9..84079663dd9 100644 --- a/code/modules/jobs/job_types/serfs/innkeep.dm +++ b/code/modules/jobs/job_types/serfs/innkeep.dm @@ -44,6 +44,7 @@ exp_requirements = list( EXP_TYPE_LIVING = 300 ) + book_type = /obj/item/recipe_book/cooking /datum/outfit/innkeep name = JOB_INNKEEP diff --git a/code/modules/jobs/job_types/serfs/mason.dm b/code/modules/jobs/job_types/serfs/mason.dm index 666547fb438..ac967414fce 100644 --- a/code/modules/jobs/job_types/serfs/mason.dm +++ b/code/modules/jobs/job_types/serfs/mason.dm @@ -41,6 +41,7 @@ job_bitflag = BITFLAG_CONSTRUCTOR attribute_sheet = /datum/attribute_holder/sheet/job/mason + book_type = /obj/item/recipe_book/masonry /datum/job/mason/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() diff --git a/code/modules/jobs/job_types/serfs/tailor.dm b/code/modules/jobs/job_types/serfs/tailor.dm index d6e3ee9121e..bda445cd626 100644 --- a/code/modules/jobs/job_types/serfs/tailor.dm +++ b/code/modules/jobs/job_types/serfs/tailor.dm @@ -48,6 +48,7 @@ traits = list( TRAIT_SEEPRICES ) + book_type = /obj/item/recipe_book/sewing /datum/outfit/tailor name = JOB_TAILOR diff --git a/code/modules/jobs/job_types/youngfolk/innkeepchild.dm b/code/modules/jobs/job_types/youngfolk/innkeepchild.dm index 81baad85eee..74acf353677 100644 --- a/code/modules/jobs/job_types/youngfolk/innkeepchild.dm +++ b/code/modules/jobs/job_types/youngfolk/innkeepchild.dm @@ -37,6 +37,7 @@ job_bitflag = BITFLAG_CONSTRUCTOR attribute_sheet = /datum/attribute_holder/sheet/job/innkeep_son + book_type = /obj/item/recipe_book/cooking /datum/outfit/innkeep_son name = JOB_INNKEEP_SON diff --git a/code/modules/mob/living/carbon/human/update_icons.dm b/code/modules/mob/living/carbon/human/update_icons.dm index 0aeb206fc6d..fa8214ac287 100644 --- a/code/modules/mob/living/carbon/human/update_icons.dm +++ b/code/modules/mob/living/carbon/human/update_icons.dm @@ -1086,7 +1086,7 @@ GLOBAL_PROTECT(no_child_icons) S.pixel_y += offsets[OFFSET_SHIRT][2] overlays_standing[SHIRTSLEEVE_LAYER] = sleeves - update_body_parts(redraw = TRUE) + update_body_parts() dna.species.handle_body(src) update_body() @@ -1149,7 +1149,7 @@ GLOBAL_PROTECT(no_child_icons) S.pixel_y += offsets[OFFSET_ARMOR][2] overlays_standing[ARMORSLEEVE_LAYER] = sleeves - update_body_parts(redraw = TRUE) + update_body_parts() dna.species.handle_body(src) update_body() update_inv_shirt() // fix boob diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 3b3a2fb0964..bf87e3d0283 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -565,6 +565,11 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(HAS_TRAIT(src, TRAIT_POISONBITE)) butcher_results += /obj/item/reagent_containers/food/snacks/poisonglands butcher_results[/obj/item/reagent_containers/food/snacks/poisonglands] = 1 + botched_butcher_results += /obj/item/reagent_containers/food/snacks/poisonglands + botched_butcher_results[/obj/item/reagent_containers/food/snacks/poisonglands] = 1 + perfect_butcher_results += /obj/item/reagent_containers/food/snacks/poisonglands + perfect_butcher_results[/obj/item/reagent_containers/food/snacks/poisonglands] = 1 + for(var/path in butcher_results) var/amount = max(1, round(butcher_results[path] * genetic_butcher_scale, 1)) if(!do_after(user, time_per_cut, target = src)) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index d219507b016..833e4b1102b 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -237,7 +237,7 @@ */ /obj/projectile/proc/on_hit(atom/target, blocked = FALSE, pierce_hit) if(fired_from) - SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_ON_HIT, firer, target, Angle) + SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_ON_HIT, firer, target, Angle, def_zone, damage) SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle) var/turf/target_loca = get_turf(target) diff --git a/code/modules/reagents/chemistry/reagents/special_reagents.dm b/code/modules/reagents/chemistry/reagents/special_reagents.dm index d41a6517294..c1bf99dc22d 100644 --- a/code/modules/reagents/chemistry/reagents/special_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/special_reagents.dm @@ -71,7 +71,7 @@ /datum/reagent/drug/phlogiston_elasticum/on_mob_end_metabolize(mob/living/L) . = ..() - L.remove_chem_effect(CE_BOUNCY, 4) + L.remove_chem_effect(CE_BOUNCY, "[type]") /datum/reagent/drug/gravitum_elixir name = "Gravitum Elixir" @@ -104,7 +104,7 @@ /datum/reagent/drug/subtilum_tincture/on_mob_end_metabolize(mob/living/L) . = ..() - L.remove_chem_effect(CE_SHRINKING, 4) + L.remove_chem_effect(CE_SHRINKING, "[type]") L.update_effect_scaling() /datum/reagent/sal_petris diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm index bca2a35f58b..73797596218 100644 --- a/code/modules/reagents/chemistry/recipes.dm +++ b/code/modules/reagents/chemistry/recipes.dm @@ -1,4 +1,5 @@ /datum/chemical_reaction + abstract_type = /datum/chemical_reaction var/name = null var/id = null var/list/results = new/list() diff --git a/code/modules/reagents/chemistry/recipes/buffs.dm b/code/modules/reagents/chemistry/recipes/buffs.dm index 35b3426502d..f2b70526611 100644 --- a/code/modules/reagents/chemistry/recipes/buffs.dm +++ b/code/modules/reagents/chemistry/recipes/buffs.dm @@ -31,7 +31,7 @@ required_reagents = list( /datum/reagent/buff/speed/concentrated = 3, /datum/reagent/buff/perception/concentrated = 3, - /datum/reagent/toxin/fyritiusnectar + /datum/reagent/toxin/fyritiusnectar = 1 ) results = list(/datum/reagent/skill_elixir/whip_hand = 10) @@ -67,7 +67,7 @@ required_reagents = list( /datum/reagent/buff/intelligence/concentrated = 4, /datum/reagent/ash = 3, - /datum/reagent/toxin/fyritiusnectar, + /datum/reagent/toxin/fyritiusnectar = 1, ) results = list(/datum/reagent/skill_elixir/craftsmans_wit = 10) diff --git a/code/modules/reagents/chemistry/recipes/distillation/_base.dm b/code/modules/reagents/chemistry/recipes/distillation/_base.dm index c6c32b93a3d..ffc442a4897 100644 --- a/code/modules/reagents/chemistry/recipes/distillation/_base.dm +++ b/code/modules/reagents/chemistry/recipes/distillation/_base.dm @@ -2,6 +2,7 @@ /// Unlike chemical reactions (which trigger on mixing), these only fire when the /// separator is actively boiling the correct reagent above required_temp. /datum/distillation_recipe + abstract_type = /datum/distillation_recipe var/name = null var/id = null /// The primary reagent that gets distilled (vaporized). Must match separating_reagent. diff --git a/code/modules/reagents/chemistry/recipes/distillation/misc.dm b/code/modules/reagents/chemistry/recipes/distillation/misc.dm index ef2d561c913..70c231a2d7a 100644 --- a/code/modules/reagents/chemistry/recipes/distillation/misc.dm +++ b/code/modules/reagents/chemistry/recipes/distillation/misc.dm @@ -11,7 +11,7 @@ distill_message = "The water boils away leaving salt." /datum/distillation_recipe/mushroom_toxin - name = "Mushroom Toxin" + name = "Amanitin" id = "mush_tox" distilled_reagent = /datum/reagent/caveweep consume_reagents = TRUE @@ -22,7 +22,7 @@ distill_message = "The tears turn a pure white and release a foul stench." /datum/distillation_recipe/concentrated_mush_toxin - name = "Concentrated Mushroom Toxin" + name = "Amatoxin" id = "mush_tox_conc" distilled_reagent = /datum/reagent/toxin/amanitin consume_reagents = TRUE diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm index 341434f1b61..606664f1bb1 100644 --- a/code/modules/reagents/reagent_containers.dm +++ b/code/modules/reagents/reagent_containers.dm @@ -156,7 +156,7 @@ /obj/item/reagent_containers/attackby_secondary(obj/item/I, mob/living/user, list/modifiers) . = ..() - if(GetComponent(/datum/component/storage)) + if(GetComponent(/datum/component/storage) || !soaker) return if(!is_open_container() || !reagents || !reagents.total_volume && soaker) to_chat(user, span_warning("\The [src] needs to be open and have reagents to soak something in.")) @@ -200,7 +200,7 @@ /obj/item/reagent_containers/attackby(obj/item/I, mob/living/user, list/modifiers) . = ..() - if(is_open_container() && reagents && reagents.total_volume > 0 && !GetComponent(/datum/component/storage)) + if(is_open_container() && reagents && reagents.total_volume > 0 && !GetComponent(/datum/component/storage) && soaker) if(!istype(I, /obj/item/reagent_containers) && !istype(I, /obj/item/paper)) var/splash_amount = reagents.total_volume * 0.05 if(splash_amount < 1) diff --git a/code/modules/reagents/reagent_containers/lux.dm b/code/modules/reagents/reagent_containers/lux.dm index 2293d67658d..64f1ecf3979 100644 --- a/code/modules/reagents/reagent_containers/lux.dm +++ b/code/modules/reagents/reagent_containers/lux.dm @@ -139,6 +139,7 @@ volume = 15 list_reagents = list(/datum/reagent/lux_tainted = 5) grind_results = list(/datum/reagent/lux_tainted = 5) + indexed = TRUE sellprice = 25 /datum/reagent/lux_tainted diff --git a/code/modules/reagents/reagent_containers/mortar.dm b/code/modules/reagents/reagent_containers/mortar.dm index 1d4a1b475f9..441214a99a9 100644 --- a/code/modules/reagents/reagent_containers/mortar.dm +++ b/code/modules/reagents/reagent_containers/mortar.dm @@ -57,7 +57,7 @@ // Check all items can actually be juiced/ground for(var/obj/item/I in to_grind) - if(!I.grind_results && !I.juice_results || !I.reagents.total_volume) + if(!I.grind_results && !I.juice_results && !I.reagents?.total_volume) to_chat(user, span_warning("I cannot process [I] this way.")) return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN diff --git a/code/modules/reagents/reagent_containers/powder.dm b/code/modules/reagents/reagent_containers/powder.dm index 0c06346f141..53c682c0b42 100644 --- a/code/modules/reagents/reagent_containers/powder.dm +++ b/code/modules/reagents/reagent_containers/powder.dm @@ -18,21 +18,21 @@ return null var/list/data = list() - data["type"] = "snack_processing" - data["name"] = name - data["category"] = "Processing" + data["type"] = "snack_processing" + data["name"] = name + data["category"] = "Processing" data["_output_path"] = "[type]" - data["output_name"] = name - data["output_icon"] = "[icon]" + data["output_name"] = name + data["output_icon"] = "[icon]" data["output_state"] = "[icon_state]" var/list/milled_from = list() for(var/atom/src_path as anything in milled_from_paths) milled_from += list(list( - "name" = initial(src_path.name), - "icon" = "[initial(src_path.icon)]", + "name" = initial(src_path.name), + "icon" = "[initial(src_path.icon)]", "icon_state" = "[initial(src_path.icon_state)]", - "_path" = "[src_path]", + "_path" = "[src_path]", )) data["milled_from"] = milled_from return data diff --git a/code/modules/spells/_spell_tree/learn_spell.dm b/code/modules/spells/_spell_tree/learn_spell.dm index 339d2b8156b..5e0be2371d5 100644 --- a/code/modules/spells/_spell_tree/learn_spell.dm +++ b/code/modules/spells/_spell_tree/learn_spell.dm @@ -5,6 +5,7 @@ sound = null school = SCHOOL_TRANSMUTATION + spell_flags = SPELL_UNETCHABLE charge_required = FALSE has_visual_effects = FALSE diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index 2860653e658..1f6d74dfdb2 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -61,6 +61,9 @@ /// Cost to cast based on [spell_type]. var/spell_cost = 0 + ///this is purely for etching + var/spell_tier = 1 + /// The sound played on cast. var/sound = 'sound/magic/whiteflame.ogg' diff --git a/code/modules/surgery/bodyparts/bodypart_wounds.dm b/code/modules/surgery/bodyparts/bodypart_wounds.dm index 40099365a73..24abb26c0da 100644 --- a/code/modules/surgery/bodyparts/bodypart_wounds.dm +++ b/code/modules/surgery/bodyparts/bodypart_wounds.dm @@ -170,7 +170,7 @@ if(!bclass || !dam || !owner || (owner.status_flags & GODMODE)) return dam *= damage_multiplier - if(dam < 5) + if(dam < 5 && bclass != WOUND_INTERNAL_BRUISE) if(CEILING(dam, 1) < 5) return dam = CEILING(dam, 1) @@ -214,7 +214,7 @@ if(wounding_type == WOUND_NONE) return - if((zone_precise in list(BODY_ZONE_PRECISE_L_EYE, BODY_ZONE_PRECISE_L_EYE)) && wounding_type == WOUND_PIERCE) + if((zone_precise in list(BODY_ZONE_PRECISE_L_EYE, BODY_ZONE_PRECISE_R_EYE)) && wounding_type == WOUND_PIERCE) organ_bonus = CANT_ORGAN if(organ_bonus != CANT_ORGAN) diff --git a/code/modules/surgery/organs/organ_processing/lungs.dm b/code/modules/surgery/organs/organ_processing/lungs.dm index 7ad0135b6ef..8e8c9c1a4a1 100644 --- a/code/modules/surgery/organs/organ_processing/lungs.dm +++ b/code/modules/surgery/organs/organ_processing/lungs.dm @@ -14,7 +14,7 @@ /datum/organ_process/lungs/proc/handle_oxygenation(mob/living/carbon/owner, delta_time, times_fired) var/lung_efficiency = owner.getorganslotefficiency(ORGAN_SLOT_LUNGS) - var/effective_oxygenation = ((130 - owner.getOxyLoss()) * (lung_efficiency/optimal_threshold)) + var/effective_oxygenation = ((100 - owner.getOxyLoss()) * (lung_efficiency/optimal_threshold)) if(effective_oxygenation < owner.total_oxygen_req) if(DT_PROB(0.5, delta_time)) if(owner.body_position != LYING_DOWN) diff --git a/code/modules/unit_tests/craftable_clothes.dm b/code/modules/unit_tests/craftable_clothes.dm index c4eac980b73..a3dfc6d49b6 100644 --- a/code/modules/unit_tests/craftable_clothes.dm +++ b/code/modules/unit_tests/craftable_clothes.dm @@ -182,6 +182,10 @@ abstract types are automatically excluded. for(var/datum/artificer_recipe/recipe as anything in subtypesof(/datum/artificer_recipe)) clothes_list -= initial(recipe.created_item) + // artificer recipes + for(var/datum/arcyne_crafting_recipe/recipe as anything in subtypesof(/datum/arcyne_crafting_recipe)) + clothes_list -= initial(recipe.output) + if(!clothes_list.len) return diff --git a/icons/effects/64x64.dmi b/icons/effects/64x64.dmi index 0b2d837f5d1..cbe3feebb23 100644 Binary files a/icons/effects/64x64.dmi and b/icons/effects/64x64.dmi differ diff --git a/icons/effects/vampire/96x96.dmi b/icons/effects/vampire/96x96.dmi index 9feb39da3c2..7a42dd507b1 100644 Binary files a/icons/effects/vampire/96x96.dmi and b/icons/effects/vampire/96x96.dmi differ diff --git a/icons/roguetown/items/wands.dmi b/icons/roguetown/items/wands.dmi new file mode 100644 index 00000000000..cbca8c02c17 Binary files /dev/null and b/icons/roguetown/items/wands.dmi differ diff --git a/tgui/packages/tgui/interfaces/RecipeBook.tsx b/tgui/packages/tgui/interfaces/RecipeBook.tsx index 96ad735ff88..5b0f552d84d 100644 --- a/tgui/packages/tgui/interfaces/RecipeBook.tsx +++ b/tgui/packages/tgui/interfaces/RecipeBook.tsx @@ -1,1627 +1,16 @@ import { useBackend } from '../backend'; import { useLocalState } from '../backend'; -import { Box, Button, DmIcon, Icon, Input, Stack, Tooltip } from 'tgui-core/components'; import { Window } from '../layouts'; - -interface IconData { - icon: string; - icon_state: string; -} - -interface ItemRef extends IconData { - name: string; - count?: number; - any?: boolean; - _path?: string; -} - -interface ReagentRef { - name: string; - amount: number; -} - -interface SurgeryStep { - name: string; - desc?: string; - tools?: { name: string; chance: number }[]; - accept_hand?: boolean; - accept_any?: boolean; - self_operable?: boolean; - lying_required?: boolean; - repeating?: boolean; - ignore_clothes?: boolean; - skill_name?: string; - skill_min?: string; - skill_median?: string; - chems?: string; - organs?: string[]; - flags?: string[]; -} - -interface PotteryStep extends IconData { - name: string; - time_s: number; -} - -interface SlapcraftStep extends IconData { - name: string; - desc: string; - optional: boolean; - verb: string; - index: number; - recipe_link?: string; - _path?: string; -} - -interface NodeEntry { - name: string; - likelihood: string; -} - -interface AgeStage { - name: string; - time_s: number; -} - -interface EssenceEntry { - name: string; - amount: number; -} - -interface Recipe { - type: string; - name: string; - category: string; - // shared output fields - output_name?: string; - output_icon?: string; - output_state?: string; - output_count?: number; - _output_path?: string; - _extra_output_paths?: string[]; - yield_names?: string[]; // natural_precursor: essences this yields - // repeatable - requirements?: ItemRef[]; - tools?: ItemRef[]; - reagents?: ReagentRef[]; - skill_name?: string; - skill_level?: string; - skill_required?: boolean | string; - starting_name?: string; - starting_icon?: string; - starting_state?: string; - attacked_name?: string; - attacked_icon?: string; - attacked_state?: string; - allow_inverse?: boolean; - // orderless_slapcraft / slapcraft - steps?: (SlapcraftStep | PotteryStep | SurgeryStep)[]; - finishing_name?: string; - finishing_icon?: string; - finishing_state?: string; - // blueprint - desc?: string; - materials?: ItemRef[]; - tool_name?: string; - tool_icon?: string; - tool_state?: string; - _tool_path?: string; - skill_diff?: number; - build_time?: number; - supports_directions?: boolean; - floor_object?: boolean; - // container_craft - craft_verb?: string; - crafting_time?: number; - wildcards?: { name: string; count: number }[]; - max_optionals?: number; - opt_items?: ItemRef[]; - opt_wildcards?: { name: string; count: number }[]; - container_name?: string; - container_icon?: string; - container_state?: string; - extra_html?: string; - // molten - temperature_c?: number; - outputs?: { name: string; count: number }[]; - // anvil / artificer - bar_name?: string; - bar_icon?: string; - bar_state?: string; - base_name?: string; - base_icon?: string; - base_state?: string; - extras?: ItemRef[]; - // brewing - brew_time_s?: number; - hints?: string; - heat_c?: number; - prereq_name?: string; - ages?: boolean; - crops?: ItemRef[]; - items?: ItemRef[]; - output_liquid?: string; - output_volume?: number; - output_item_name?: string; - output_item_icon?: string; - output_item_state?: string; - output_item_count?: number; - age_stages?: AgeStage[]; - // runeritual - tier?: number; - // book_entry - html?: string; - // alch_cauldron - essences?: EssenceEntry[]; - output_reagents?: { name: string; amount: number }[]; - output_items?: ItemRef[]; - smells_like?: string; - // essence_combination - inputs?: EssenceEntry[]; - output_amount?: number; - // essence_infusion - target_name?: string; - target_icon?: string; - target_state?: string; - result_name?: string; - result_icon?: string; - result_state?: string; - infusion_time?: number; - // natural_precursor - yields?: EssenceEntry[]; - splits_from?: string[]; - splits_from_paths?: string[]; - search_data?: string; - // plant_def - maturation_min?: number; - produce_min?: number; - yield_min?: number; - yield_max?: number; - perennial?: boolean; - water_drain?: number; - weed_immune?: boolean; - underground?: boolean; - family?: string; - nitrogen_req?: number; - phosphorus_req?: number; - potassium_req?: number; - nitrogen_prod?: number; - phosphorus_prod?: number; - potassium_prod?: number; - // surgery - heretical?: boolean; - req_bodypart?: boolean; - req_missing_bodypart?: boolean; - req_real_bodypart?: boolean; - // wound - severity_text?: string; - severity_color?: string; - critical?: boolean; - mortal?: boolean; - disabling?: boolean; - whp?: number; - can_sew?: boolean; - can_cauterize?: boolean; - sew_threshold?: number; - sewn_whp?: number; - bleed_rate?: number; - sewn_bleed_rate?: number; - clotting_rate?: number; - clotting_threshold?: number; - sewn_clotting_rate?: number; - sewn_clotting_threshold?: number; - passive_healing?: number; - sleep_healing?: number; - woundpain?: number; - sewn_woundpain?: number; - special_props?: string[]; - check_name?: string; - // chimeric_node - slot_name?: string; - slot_color?: string; - is_special?: boolean; - allowed_slots?: string[]; - forbidden_slots?: string[]; - // chimeric_table - node_tier?: number; - purity_min?: number; - purity_max?: number; - base_blood_cost?: number; - pref_bonus?: number; - incompat_penalty?: number; - preferred_blood?: string[]; - compatible_blood?: string[]; - incompatible_blood?: string[]; - input_nodes?: NodeEntry[]; - output_nodes?: NodeEntry[]; - special_nodes?: NodeEntry[]; - source_mobs?: { name: string; icon: string; icon_state: string; _path: string }[]; - // snack_processing - mill_name?: string; - mill_icon?: string; - mill_state?: string; - mill_path?: string; - grind_results?: { name: string; amount: number }[]; - juice_results?: { name: string; amount: number }[]; - milled_from?: ItemRef[]; - sliced_from?: ItemRef[] - slice_name?: string; - slice_icon?: string; - slice_state?: string; - slice_path?: string; - slice_num?: number; - slice_skill?: string; - // fish - avg_size?: number; - avg_weight?: number; - fluid_type?: string; - temp_min?: number; - temp_max?: number; - spots?: string; - difficulty?: string; - fav_bait?: string; - dislike_bait?: string; - lures?: string[]; - traits?: string[]; - // obtained_from - sources?: { label: string; _path: string; name: string; icon: string; icon_state: string }[]; - // source_page - drops?: { name: string; icon: string; icon_state: string; _path: string; source_label: string }[]; - // pottery - speed_sweetspot?: string | number; - // organ - zone?: string; - threshold_low?: number; - threshold_high?: number; - threshold_max?: number; - msg_bruised?: string; - msg_broken?: string; - msg_bruised_healed?: string; - msg_broken_healed?: string; - msg_failing?: string; - msg_fixed?: string; - healing_factor?: number; - healing_items?: ItemRef[]; - healing_tools?: string[]; - attaching_items?: ItemRef[]; - blood_req?: number; - oxygen_req?: number; - nutriment_req?: number; - hydration_req?: number; -} - -interface RecipeBookData { - book_name: string; - book_desc: string; - recipes: Recipe[]; - linked_recipes: Recipe[]; -} - -const Sprite = (props: { icon?: string; icon_state?: string; size?: number }) => { - const { icon, icon_state, size = 2 } = props; - if (!icon || !icon_state) return null; - const fallback = ; - return ( - - ); -}; - -const RecipeLink = (props: { - name: string | null | undefined; - path?: string; - lookup: Map; - pickerMap: Map; - allRecipes: Recipe[]; - essenceIndex?: Map; - onNavigate: (r: Recipe) => void; -}) => { - const { name, path, lookup, pickerMap, allRecipes, essenceIndex, onNavigate } = props; - if (!name) return null; - - const pickerByPath = (path && pickerMap.get(path)) || []; - const pickerByName = pickerMap.get(name.toLowerCase()) || []; - const essenceByName = (!path && essenceIndex?.get(name.toLowerCase())) || []; - const merged = [...new Set([...(pickerByPath.length ? pickerByPath : pickerByName), ...essenceByName])]; - if (merged.length > 1) { - return ; - } - if (merged.length === 1 && !path && !lookup.get(name.toLowerCase())) { - const sole = merged[0]; - return onNavigate(sole)} title={`Go to: ${sole.name}`}>{name}; - } - - let target: Recipe | undefined = lookup.get(name.toLowerCase()); - - if (!target && path) target = lookup.get(path); - - let subtypeMatches: Recipe[] = []; - if (!target && path) { - const prefix = path + '/'; - subtypeMatches = allRecipes.filter( - (r) => r._output_path && r._output_path.startsWith(prefix) - && !r._output_path.substring(prefix.length).includes('/') - ); - if (subtypeMatches.length === 1) target = subtypeMatches[0]; - } - - let essenceMatches: Recipe[] = []; - if (!target && subtypeMatches.length === 0 && essenceIndex && !path) { - essenceMatches = essenceIndex.get(name.toLowerCase()) || []; - if (essenceMatches.length === 1) target = essenceMatches[0]; - } - - const pickerOptions = subtypeMatches.length > 1 ? subtypeMatches - : essenceMatches.length > 1 ? essenceMatches - : []; - - if (!target && pickerOptions.length === 0) return {name}; - if (!target && pickerOptions.length > 1) { - return ; - } - - return ( - onNavigate(target!)} title={`Go to: ${target!.name}`}> - {name} - - ); -}; - -const recipeTypeLabel = (type: string): string => { - const labels: Record = { - repeatable: 'Crafting', brewing: 'Brewing', blueprint: 'Blueprint', - container_craft: 'Cooking', molten: 'Smelting', anvil: 'Smithing', - artificer: 'Artificer', pottery: 'Pottery', runeritual: 'Ritual', - book_entry: 'Lore', alch_cauldron: 'Alchemy', essence_combination: 'Essence', - essence_infusion: 'Infusion', natural_precursor: 'Precursor', - plant_def: 'Farming', surgery: 'Surgery', wound: 'Wound', - chimeric_node: 'Chimeric', chimeric_table: 'Humor', - fish: 'Fish', slapcraft: 'Crafting', orderless_slapcraft: 'Crafting', - snack_processing: 'Processing', - obtained_from: 'Source', - source_page: 'Source', organ: 'Organ', - }; - return labels[type] || type; -}; - -const RecipePicker = (props: { - name: string; - options: Recipe[]; - onNavigate: (r: Recipe) => void; -}) => { - const { name, options, onNavigate } = props; - const [open, setOpen] = useLocalState(`picker_${name}`, false); - const [coords, setCoords] = useLocalState<{ top: number; left: number } | null>(`picker_coords_${name}`, null); - - const handleClick = (e: any) => { - const rect = e.currentTarget.getBoundingClientRect(); - setCoords({ top: rect.bottom + 2, left: rect.left }); - setOpen(!open); - }; - - return ( - - - {name} ▾ - - {open && coords && ( - - {options.map((r, i) => ( - { setOpen(false); onNavigate(r); }}> - {r.output_name || r.name} - {recipeTypeLabel(r.type)} - - ))} - - )} - - ); -}; - -const ItemRow = (props: { - item: ItemRef; - lookup: Map; - pickerMap: Map; - allRecipes: Recipe[]; - essenceIndex: Map; - onNavigate: (r: Recipe) => void; -}) => { - const { item, lookup, pickerMap, allRecipes, essenceIndex, onNavigate } = props; - return ( - - - {item.any ? 'any ' : ''} - {item.count !== undefined && item.count !== 1 ? `${item.count}× ` : ''} - - - ); -}; - -const SectionHead = (props: { children: any }) => ( - {props.children} -); - -const HR = () => ; - -const Badge = (props: { color?: string; children: any }) => ( - - {props.children} - -); - -const WarnFlag = (props: { color: string; children: any }) => ( - ⚠ {props.children} -); - -const OutputBanner = (props: { - icon?: string; - icon_state?: string; - name: string; - count?: number; - lookup: Map; - pickerMap: Map; - allRecipes: Recipe[]; - essenceIndex: Map; - onNavigate: (r: Recipe) => void; -}) => { - const { icon, icon_state, name, count, lookup, pickerMap, allRecipes, essenceIndex, onNavigate } = props; - return ( - - Creates - - - {count !== undefined && count > 1 ? `${count}× ` : ''} - - - - ); -}; - -const DetailOrgan = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {r.zone && Located in: {r.zone}} - - Damage Thresholds - - {r.threshold_low !== undefined && ( - - Bruised: {r.threshold_low} - {r.msg_bruised && } - - )} - {r.threshold_high !== undefined && ( - - Failing: {r.threshold_high} - {r.msg_broken && } - - )} - {r.threshold_max !== undefined && ( - - Destroyed: {r.threshold_max} - {r.msg_failing && } - - )} - - - {(r.msg_bruised_healed || r.msg_broken_healed || r.msg_fixed) && ( - <> - Recovery Messages - - {r.msg_bruised_healed && } - {r.msg_broken_healed && } - {r.msg_fixed && } - - > - )} - - Healing - - {r.healing_factor !== undefined && ( - Passive healing: {r.healing_factor}/s - )} - {!!r.healing_tools?.length && ( - Tools: {r.healing_tools.join(', ')} - )} - - {!!r.healing_items?.length && ( - <> - Healing Items - {r.healing_items.map((item, i) => ( - - ))} - > - )} - - {!!r.attaching_items?.length && ( - <> - Reattachment Items - {r.attaching_items.map((item, i) => ( - - ))} - > - )} - - {(r.blood_req || r.oxygen_req || r.nutriment_req || r.hydration_req) ? ( - <> - Body Requirements - - {!!r.blood_req && Blood: {r.blood_req} ligulae a breath} - {!!r.oxygen_req && Oxygen: {r.oxygen_req} ligulae a breath} - {!!r.nutriment_req && Nutriment: {r.nutriment_req} ligulae a breath} - {!!r.hydration_req && Hydration: {r.hydration_req} ligulae a breath} - - > - ) : null} - > -); - -const DetailRepeatable = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {r.skill_level && ( - - {r.skill_required ? '⚑ Required: ' : '☆ Recommended: '} - {r.skill_name} - - )} - {!!r.requirements?.length && ( - <> - Materials - {r.requirements.map((item, i) => )} - > - )} - {!!r.tools?.length && ( - <> - Tools - {r.tools.map((item, i) => )} - > - )} - {!!r.reagents?.length && ( - <> - Liquids - {r.reagents.map((rg, i) => ( - - {rg.amount}u of - - ))} - > - )} - Steps - - - - Use {r.starting_name} - - - - on {r.attacked_name} - - {!!r.allow_inverse && or vice versa} - - {r.output_name && ( - - )} - > -); - -const DetailBrewing = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - ⏱ {r.brew_time_s}s brewing time - {r.heat_c !== undefined && Requires heated vessel ≥ {Math.round(r.heat_c!)}°C} - {r.prereq_name && Requires {r.prereq_name} present in keg} - {!!r.ages && Will continue to age after brewing} - {r.hints && 💡 {r.hints}} - {!!(r.crops?.length || r.items?.length) && ( - <> - Items Required - {r.crops?.map((item, i) => )} - {r.items?.map((item, i) => )} - > - )} - {!!r.reagents?.length && ( - <> - Liquids Required - {r.reagents.map((rg, i) => ( - - {rg.amount}u of - - ))} - > - )} - Output - {r.output_liquid && ( - - Liquid - {r.output_volume}u of {r.output_liquid} - - )} - {r.output_item_name && ( - - )} - {!!r.age_stages?.length && ( - <> - Aging - {r.age_stages!.map((ag, i) => ( - - After {ag.time_s}s → - - ))} - > - )} - > -); - -const DetailBlueprint = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {r.desc && } - {!!r.materials?.length && ( - <> - Materials - {r.materials!.map((item, i) => )} - > - )} - Construction - - - - Tool: - - {r.skill_name && ( - - Skill: {r.skill_name}{r.skill_diff !== undefined ? ` (diff ${r.skill_diff})` : ''} - - )} - ⏱ {r.build_time}s - {!!r.supports_directions && ↻ Supports rotation} - {!!r.floor_object && ▣ Full floor tile} - - {r.output_name && ( - - )} - > -); - -const DetailContainerCraft = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {!!r.requirements?.length && ( - <> - Items - {r.requirements!.map((item, i) => )} - > - )} - {!!r.reagents?.length && ( - <> - Liquids - {r.reagents!.map((rg, i) => ( - - {rg.amount}u of - - ))} - > - )} - {!!r.wildcards?.length && ( - <> - Alternative Items - {r.wildcards!.map((wc, i) => ( - {wc.count}× any {wc.name} - ))} - > - )} - {r.max_optionals !== undefined && r.max_optionals > 0 && ( - <> - Optional (max {r.max_optionals}) - {r.opt_items?.map((item, i) => )} - {r.opt_wildcards?.map((wc, i) => ( - up to {wc.count}× any {wc.name} - ))} - > - )} - Process - - {r.craft_verb} for {r.crafting_time}s - {r.container_name && ( - - - inside a {r.container_name} - - )} - - {r.extra_html && } - {r.output_name && ( - - Creates - - {r.output_count !== undefined && r.output_count > 1 ? `${r.output_count}× ` : ''} - - - - )} - > -); - -const DetailMolten = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - Materials (molten) - {r.materials?.map((m, i) => ( - - {m.count} parts molten - - ))} - - - 🌡 Heat to {r.temperature_c !== undefined ? `${Math.round(r.temperature_c!)}°C` : '—'} - - - {!!r.outputs?.length && ( - <> - Output - {r.outputs!.map((o, i) => ( - - {o.count} parts - - ))} - > - )} - > -); - -const DetailAnvil = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - Steps - - - - Place on anvil - - 🔨 Hammer - {r.extras?.map((item, i) => ( - - - - Add - - 🔨 Hammer - - ))} - - {r.output_name && ( - - )} - > -); - -const DetailArtificer = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - Steps - - - - Place on artificer table - - 🔨 Hammer - {r.extras?.map((item, i) => ( - - - - Add - - 🔨 Hammer - - ))} - - {r.output_name && ( - - )} - > -); - -const DetailPottery = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => { - const steps = r.steps as PotteryStep[] | undefined; - return ( - <> - ⚙ Rotational sweetspot: {r.speed_sweetspot} - Steps - - {steps?.map((s, i) => ( - - - - Add to lathe - - ↻ Spin for {s.time_s}s - - ))} - - {r.output_name && ( - - )} - > - ); -}; - -const DetailRuneRitual = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - Complexity Tier {r.tier} - {!!r.items?.length && ( - <> - Items Required - {r.items!.map((item, i) => )} - > - )} - Instructions - - - Draw the required rune with Arcyne Chalk, then supply the above items. - - - > -); - -const DetailBookEntry = ({ r }: { r: Recipe }) => ( - -); - -const DetailAlchCauldron = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - Requires 50u of Water in cauldron - {!!r.essences?.length && ( - <> - Essences Required - {r.essences!.map((e, i) => ( - - {e.amount} parts - - ))} - > - )} - {!!r.output_reagents?.length && ( - <> - Output Reagents - {r.output_reagents!.map((rg, i) => ( - - {rg.amount}u of - - ))} - > - )} - {!!r.output_items?.length && ( - <> - Output Items - {r.output_items!.map((item, i) => )} - > - )} - {r.smells_like && 🌿 Smells like: {r.smells_like}} - > -); - -const DetailEssenceCombination = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {!!r.inputs?.length && ( - <> - Input Essences - {r.inputs!.map((e, i) => ( - - {e.amount} parts - - ))} - > - )} - {r.output_name && ( - - )} - {r.skill_required && typeof r.skill_required === 'string' && ( - Skill required: {r.skill_required} - )} - > -); - -const DetailEssenceInfusion = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - Target Item - - - - - {!!r.essences?.length && ( - <> - Essences - {r.essences!.map((e, i) => ( - - {e.amount} parts - - ))} - > - )} - - ⏱ Infusion time: {r.infusion_time}s - - {r.result_name && ( - - )} - > -); - -const DetailNaturalPrecursor = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {!!r.yields?.length && ( - <> - Essence Yields - {r.yields!.map((y, i) => ( - - {y.amount} - - ))} - > - )} - {!!r.splits_from?.length && ( - <> - Splits From - {r.splits_from!.map((s, i) => ( - - - - ))} - > - )} - > -); - -const DetailPlantDef = ({ r }: { r: Recipe }) => ( - <> - Growth - - Maturation: {r.maturation_min} min - Produce interval: {r.produce_min} min - Yield: {r.yield_min}–{r.yield_max} - {r.perennial ? '♻ Perennial' : '1× Annual'} - 💧 Water drain: {r.water_drain}u/min - {!!r.weed_immune && 🌿 Weed immune} - {!!r.underground && ⛏ Can grow underground} - Family: {r.family} - - {!!(r.nitrogen_req || r.phosphorus_req || r.potassium_req) && ( - <> - Nutrient Requirements - - {r.nitrogen_req ? N: {r.nitrogen_req}u : null} - {r.phosphorus_req ? P: {r.phosphorus_req}u : null} - {r.potassium_req ? K: {r.potassium_req}u : null} - - > - )} - {!!(r.nitrogen_prod || r.phosphorus_prod || r.potassium_prod) && ( - <> - Soil Enrichment - - {r.nitrogen_prod ? +N: {r.nitrogen_prod}u : null} - {r.phosphorus_prod ? +P: {r.phosphorus_prod}u : null} - {r.potassium_prod ? +K: {r.potassium_prod}u : null} - - > - )} - > -); - -const DetailSurgery = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => { - const steps = r.steps as SurgeryStep[] | undefined; - return ( - <> - {!!r.heretical && HERETICAL RESEARCH} - {r.desc && } - {!!r.req_bodypart && Requires bodypart to be present} - {!!r.req_missing_bodypart && Requires bodypart to be MISSING} - {!!r.req_real_bodypart && Cannot be performed on prosthetics} - Procedure - {steps?.map((s, i) => ( - - Step {i + 1}: {s.name} - {s.desc && } - {!!s.tools?.length && ( - - Tools: - {s.tools!.map((t, ti) => ( - - ({t.chance}%) - - ))} - - )} - {s.skill_name && ( - - Min: / Optimal: {s.skill_name} - - )} - {s.chems && Chemicals: {s.chems}} - {!!s.flags?.length && ( - - {s.flags!.map((f, fi) => • {f})} - - )} - {(!!s.accept_hand || !!s.accept_any || !s.self_operable || !!s.lying_required || !!s.repeating) && ( - - {!!s.accept_hand && Can use bare hands} - {!!s.accept_any && Accepts any item} - {!s.self_operable && Cannot self-operate} - {!!s.lying_required && Patient must be lying down} - {!!s.repeating && Repeatable until failure} - - )} - - - ))} - > - ); -}; - -const DetailWound = ({ r }: { r: Recipe }) => ( - <> - {r.desc && } - - Severity: {r.severity_text} - - {!!r.critical && CRITICAL WOUND} - {!!r.mortal && MORTAL WOUND} - {!!r.disabling && DISABLING WOUND} - Wound Stats - - WHP: {r.whp} - {r.passive_healing !== undefined && Passive healing: {r.passive_healing}/beat} - {r.sleep_healing !== undefined && Sleep healing: {r.sleep_healing}/beat} - - {(r.can_sew || r.can_cauterize) && ( - <> - Treatment - - {!!r.can_sew && ✂ Sewable ({r.sew_threshold} progress → {r.sewn_whp} WHP)} - {!!r.can_cauterize && 🔥 Can be cauterized} - - > - )} - {r.bleed_rate !== undefined && ( - <> - Bleeding - - Rate: {r.bleed_rate} - {r.sewn_bleed_rate !== undefined && Rate (sewn): {r.sewn_bleed_rate}} - {r.clotting_rate && ( - - Clotting: {r.clotting_rate}/beat{r.clotting_threshold !== undefined ? ` → ${r.clotting_threshold}` : ''} - - )} - - > - )} - {r.woundpain !== undefined && ( - <> - Pain - - - Pain: {r.woundpain}{r.sewn_woundpain !== undefined ? ` (sewn: ${r.sewn_woundpain})` : ''} - - - > - )} - {!!r.special_props?.length && ( - <> - Special Properties - - {r.special_props!.map((sp, i) => • {sp})} - - > - )} - {r.check_name && ( - <> - Diagnosis - - - - > - )} - > -); - -const DetailChimericNode = ({ r }: { r: Recipe }) => ( - <> - {r.desc && } - {r.slot_name} - {!!r.is_special && SPECIAL NODE} - Installation - - {r.allowed_slots?.length ? ( - <> - Can ONLY be installed in: - {r.allowed_slots.map((s, i) => • {s})} - > - ) : r.forbidden_slots?.length ? ( - <> - Cannot be installed in: - {r.forbidden_slots.map((s, i) => • {s})} - > - ) : ( - ✓ Can be installed in any organ - )} - - > -); - -const DetailChimericTable = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => { - const NodeList = ({ nodes, color }: { nodes?: NodeEntry[]; color: string }) => { - if (!nodes?.length) return null; - return ( - - {nodes.map((n, i) => ( - - {n.name} - — {n.likelihood} - - ))} - - ); - }; - return ( - <> - Node Info - - Max tier: {r.node_tier} - - Purity: {r.purity_min}% – {r.purity_max}% (avg {Math.round(((r.purity_min || 0) + (r.purity_max || 0)) / 2)}%) - - - Blood Cost - - Base: {r.base_blood_cost}u/beat - Preferred: −{((r.pref_bonus || 0) * 100).toFixed(0)}% - Incompatible: +{((r.incompat_penalty || 0) * 100).toFixed(0)}% - - {!!(r.preferred_blood?.length || r.compatible_blood?.length || r.incompatible_blood?.length) && ( - <> - Blood Types - - {r.preferred_blood?.map((b, i) => ★ {b})} - {r.compatible_blood?.map((b, i) => ✓ {b})} - {r.incompatible_blood?.map((b, i) => ✗ {b})} - - > - )} - Input Nodes - Output Nodes - Special Nodes - {!!r.source_mobs?.length && ( - <> - Blood Source Mobs - - {r.source_mobs.map((mob, i) => ( - - - - - ))} - - > - )} - > - ); -}; - -const DetailSnackProcessing = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {r.mill_name && ( - <> - Milling - - Mills into - - - - - - > - )} - {!!r.milled_from?.length && ( - <> - Milled From - {r.milled_from!.map((item, i) => ( - - ))} - > - )} - {!!r.sliced_from?.length && ( - <> - Sliced From - {r.sliced_from!.map((item, i) => ( - - ))} - > - )} - {!!r.sources?.length && ( - <> - Obtained From - - {r.sources!.map((s, i) => ( - - - - — {s.label} - - ))} - - > - )} - {!!r.grind_results?.length && ( - <> - Grinding - {r.grind_results!.map((rg, i) => ( - - {rg.amount}u of - - ))} - > - )} - {!!r.juice_results?.length && ( - <> - Juicing - {r.juice_results!.map((rg, i) => ( - - {rg.amount}u of - - ))} - > - )} - {r.slice_name && ( - <> - Slicing - - Slices into - - - {r.slice_num !== undefined && r.slice_num > 1 ? `${r.slice_num}× ` : ''} - - {r.slice_skill && — requires {r.slice_skill}} - - - > - )} - > -); - -const DetailObtainedFrom = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {!!r.sources?.length && ( - <> - Obtained From - - {r.sources!.map((s, i) => ( - - - - — {s.label} - - ))} - - > - )} - > -); - -const DetailSourcePage = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {!!r.drops?.length && ( - <> - Drops - - {r.drops!.map((d, i) => ( - - - - {d.source_label && — {d.source_label}} - - ))} - - > - )} - > -); - -const DetailFish = ({ r }: { r: Recipe }) => { - const diffColor = r.difficulty === 'Hard' ? '#d9534f' : r.difficulty === 'Medium' ? '#f0ad4e' : '#5cb85c'; - return ( - <> - {r.desc && } - Physical - - Size: {r.avg_size}cm - Weight: {r.avg_weight}g - - Environment - - Fluid: {r.fluid_type} - Temperature: {r.temp_min}°C – {r.temp_max}°C - - Fishing - - Found: {r.spots} - Difficulty: {r.difficulty} - Favourite bait: {r.fav_bait} - Disliked bait: {r.dislike_bait} - {r.lures?.map((l, i) => • {l})} - - {!!r.traits?.length && ( - <> - Behaviour - - {r.traits!.map((t, i) => • {t})} - - > - )} - > - ); -}; - -const DetailSlapcraft = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => { - const steps = r.steps as SlapcraftStep[] | undefined; - if (!steps) return null; - const first = steps[0]; - const second = steps[1]; - return ( - <> - Steps - - {second && (() => { - const parts = second.desc.split(second.name); - return ( - - - {parts.length > 1 ? ( - <>{parts[0]}{parts.slice(1).join(second.name)}> - ) : ( - <> {second.verb}> - )} - - ); - })()} - {first && ( - - - a - - )} - {steps.slice(2).map((s, i) => { - const descParts = s.desc.split(s.name); - return ( - - - {descParts.length > 1 ? ( - <> - {descParts[0]} - - {descParts.slice(1).join(s.name)} - > - ) : ( - <>{s.desc} > - )} - {!!s.optional && (optional)} - - ); - })} - - {r.output_name && ( - - )} - > - ); -}; - -const DetailOrderlessSlapcraft = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: { r: Recipe; lookup: Map; pickerMap: Map; allRecipes: Recipe[]; essenceIndex: Map; nav: (r: Recipe) => void }) => ( - <> - {r.skill_name && ( - With {r.skill_name} skill: - )} - Steps - - - - Start with - - then add: - - {(r.requirements as any[])?.map((req: any, i: number) => { - if (req.choices) { - return ( - - up to {req.count} of: - {req.choices.map((c: any, ci: number) => ( - - - any - - ))} - - - ); - } - return ( - - - - {req.count}× any - - - - ); - })} - {r.finishing_name && ( - - - finish with any - - )} - - > -); - -const RecipeDetail = (props: { - recipe: Recipe; - lookup: Map; - pickerMap: Map; - allRecipes: Recipe[]; - essenceIndex: Map; - onNavigate: (r: Recipe) => void; - history: Recipe[]; - onBack: () => void; -}) => { - const { recipe: r, lookup, pickerMap, allRecipes, essenceIndex, onNavigate, history, onBack } = props; - - //please someone tell me a better way then this, I don't wanna look like fucking yandere dev - const renderBody = () => { - switch (r.type) { - case 'repeatable': return ; - case 'brewing': return ; - case 'blueprint': return ; - case 'container_craft': return ; - case 'molten': return ; - case 'anvil': return ; - case 'artificer': return ; - case 'pottery': return ; - case 'runeritual': return ; - case 'book_entry': return ; - case 'alch_cauldron': return ; - case 'essence_combination': return ; - case 'essence_infusion': return ; - case 'natural_precursor': return ; - case 'plant_def': return ; - case 'surgery': return ; - case 'wound': return ; - case 'chimeric_node': return ; - case 'chimeric_table': return ; - case 'fish': return ; - case 'snack_processing': return ; - case 'obtained_from': return ; - case 'source_page': return ; - case 'slapcraft': return ; - case 'orderless_slapcraft': return ; - case 'organ': return ; - default: - return No details available for type: {r.type}; - } - }; - - return ( - - - {history.length > 0 && ( - - ← {history[history.length - 1].name} - - )} - - - - {r.name} - {r.output_count !== undefined && r.output_count > 1 ? ` ×${r.output_count}` : ''} - - {r.category} - - - {renderBody()} - - - ); -}; - -const Sidebar = (props: { - recipes: Recipe[]; - lookup: Map; - selectedRecipe: Recipe | null; - onSelect: (r: Recipe) => void; -}) => { - const { recipes, lookup, selectedRecipe, onSelect } = props; - const [search, setSearch] = useLocalState('rb_search', ''); - const [category, setCategory] = useLocalState('rb_category', 'All'); - - const categories = ['All', ...Array.from(new Set(recipes.map((r) => r.category))).sort()]; - - const filtered = recipes.filter((r) => { - const matchCat = category === 'All' || r.category === category; - const q = search.toLowerCase(); - const matchSearch = - !q || - r.name?.toLowerCase().includes(q) || - (r.search_data && r.search_data.toLowerCase().includes(q)); - return matchCat && matchSearch; - }); - - return ( - - - setSearch(value)} - className="RecipeBook__search" - /> - - - { const el = document.getElementById('rb-cat-strip'); if (el) el.scrollLeft -= 80; }}>← - - {categories.map((cat) => ( - setCategory(cat)}> - {cat} - - ))} - - { const el = document.getElementById('rb-cat-strip'); if (el) el.scrollLeft += 80; }}>→ - - - {!filtered.length && No matching recipes.} - {filtered.map((r, i) => ( - onSelect(r)}> - {r.name} - - ))} - - - ); -}; +import { Box } from 'tgui-core/components'; +import { Sidebar } from './RecipeBook/Sidebar'; +import { RecipeDetail } from './RecipeBook/RecipeDetails'; +import type { Recipe, RecipeBookData } from './RecipeBook/types'; export const RecipeBook = (props: any, context: any) => { const { data } = useBackend(); const { book_name, recipes = [], linked_recipes = [] } = data; const allRecipes = [...recipes, ...linked_recipes]; + const recipeMultiMap = new Map(); const addToMultiMap = (key: string, r: Recipe) => { if (!recipeMultiMap.has(key)) recipeMultiMap.set(key, []); @@ -1647,19 +36,20 @@ export const RecipeBook = (props: any, context: any) => { if (!essencePrecursorIndex.has(key)) essencePrecursorIndex.set(key, []); essencePrecursorIndex.get(key)!.push(r); } + if (r.type === 'snack_processing') { + for (const rg of r.grind_results || []) addToMultiMap(rg.name.toLowerCase(), r); + for (const rg of r.juice_results || []) addToMultiMap(rg.name.toLowerCase(), r); + } if (r.name) addToMultiMap(r.name.toLowerCase(), r); if (r.output_name) addToMultiMap(r.output_name.toLowerCase(), r); if (r._output_path) addToMultiMap(r._output_path, r); } const recipeLookup = new Map(); - const recipePickerMap = new Map(); // keys with >1 result + const recipePickerMap = new Map(); for (const [key, entries] of recipeMultiMap) { - if (entries.length === 1) { - recipeLookup.set(key, entries[0]); - } else { - recipePickerMap.set(key, entries); - } + if (entries.length === 1) recipeLookup.set(key, entries[0]); + else recipePickerMap.set(key, entries); } const [selectedRecipe, setSelectedRecipe] = useLocalState('rb_selected', null); @@ -1700,7 +90,9 @@ export const RecipeBook = (props: any, context: any) => { onBack={handleBack} /> ) : ( - Select a recipe from the list. + + Select a recipe from the list. + )} diff --git a/tgui/packages/tgui/interfaces/RecipeBook/Primitives.tsx b/tgui/packages/tgui/interfaces/RecipeBook/Primitives.tsx new file mode 100644 index 00000000000..42fc8e50ee1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/Primitives.tsx @@ -0,0 +1,69 @@ +import { Box, DmIcon, Icon } from 'tgui-core/components'; +import { RecipeLink } from './RecipeLink'; +import type { Recipe, ItemRef } from './types'; + +export const Sprite = (props: { icon?: string; icon_state?: string; size?: number }) => { + const { icon, icon_state, size = 2 } = props; + if (!icon || !icon_state) return null; + const fallback = ; + return ; +}; + +export const SectionHead = (props: { children: any }) => ( + {props.children} +); + +export const HR = () => ; + +export const Badge = (props: { color?: string; children: any }) => ( + + {props.children} + +); + +export const WarnFlag = (props: { color: string; children: any }) => ( + ⚠ {props.children} +); + +export const OutputBanner = (props: { + icon?: string; + icon_state?: string; + name: string; + count?: number; + lookup: Map; + pickerMap: Map; + allRecipes: Recipe[]; + essenceIndex: Map; + onNavigate: (r: Recipe) => void; +}) => { + const { icon, icon_state, name, count, lookup, pickerMap, allRecipes, essenceIndex, onNavigate } = props; + return ( + + Creates + + + {count !== undefined && count > 1 ? `${count}× ` : ''} + + + + ); +}; + +export const ItemRow = (props: { + item: ItemRef; + lookup: Map; + pickerMap: Map; + allRecipes: Recipe[]; + essenceIndex: Map; + onNavigate: (r: Recipe) => void; +}) => { + const { item, lookup, pickerMap, allRecipes, essenceIndex, onNavigate } = props; + return ( + + + {item.any ? 'any ' : ''} + {item.count !== undefined && item.count !== 1 ? `${item.count}× ` : ''} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/RecipeDetails.tsx b/tgui/packages/tgui/interfaces/RecipeBook/RecipeDetails.tsx new file mode 100644 index 00000000000..4aca1e69d2b --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/RecipeDetails.tsx @@ -0,0 +1,106 @@ +import { Box } from 'tgui-core/components'; +import { Sprite, Badge, HR } from './Primitives'; +import type { Recipe } from './types'; +import { DetailRepeatable } from './details/DetailRepeatable'; +import { DetailBrewing } from './details/DetailBrewing'; +import { DetailBlueprint } from './details/DetailBlueprint'; +import { DetailContainerCraft } from './details/DetailContainerCraft'; +import { DetailMolten } from './details/DetailMolten'; +import { DetailAnvil } from './details/DetailAnvil'; +import { DetailArtificer } from './details/DetailArtificer'; +import { DetailPottery } from './details/DetailPottery'; +import { DetailRuneRitual } from './details/DetailRuneRitual'; +import { DetailBookEntry } from './details/DetailBookEntry'; +import { DetailAlchCauldron } from './details/DetailAlchCauldron'; +import { DetailEssenceCombination } from './details/DetailEssenceCombination'; +import { DetailEssenceInfusion } from './details/DetailEssenceInfusion'; +import { DetailNaturalPrecursor } from './details/DetailNaturalPrecursor'; +import { DetailPlantDef } from './details/DetailPlantDef'; +import { DetailSurgery } from './details/DetailSurgery'; +import { DetailWound } from './details/DetailWound'; +import { DetailChimericNode } from './details/DetailChimericNode'; +import { DetailChimericTable } from './details/DetailChimericTable'; +import { DetailFish } from './details/DetailFish'; +import { DetailSnackProcessing } from './details/DetailSnackProcessing'; +import { DetailObtainedFrom } from './details/DetailObtainedFrom'; +import { DetailSourcePage } from './details/DetailSourcePage'; +import { DetailSlapcraft } from './details/DetailSlapcraft'; +import { DetailOrderlessSlapcraft } from './details/DetailOrderlessSlapcraft'; +import { DetailOrgan } from './details/DetailOrgan'; +import { DetailChemicalReaction } from './details/DetailChemicalReaction'; +import { DetailDistillation } from './details/DetailDistillation'; +import { DetailArcyneCrafting } from './details/DetailArcyneCrafting'; + +type Props = { + recipe: Recipe; + lookup: Map; + pickerMap: Map; + allRecipes: Recipe[]; + essenceIndex: Map; + onNavigate: (r: Recipe) => void; + history: Recipe[]; + onBack: () => void; +}; + +export const RecipeDetail = ({ recipe: r, lookup, pickerMap, allRecipes, essenceIndex, onNavigate, history, onBack }: Props) => { + const sharedProps = { r, lookup, pickerMap, allRecipes, essenceIndex, nav: onNavigate }; + + // please someone tell me a better way then this, I don't wanna look like fucking yandere dev + const renderBody = () => { + switch (r.type) { + case 'repeatable': return ; + case 'brewing': return ; + case 'blueprint': return ; + case 'container_craft': return ; + case 'molten': return ; + case 'anvil': return ; + case 'artificer': return ; + case 'pottery': return ; + case 'runeritual': return ; + case 'book_entry': return ; + case 'alch_cauldron': return ; + case 'essence_combination': return ; + case 'essence_infusion': return ; + case 'natural_precursor': return ; + case 'plant_def': return ; + case 'surgery': return ; + case 'wound': return ; + case 'chimeric_node': return ; + case 'chimeric_table': return ; + case 'fish': return ; + case 'snack_processing': return ; + case 'obtained_from': return ; + case 'source_page': return ; + case 'slapcraft': return ; + case 'orderless_slapcraft': return ; + case 'organ': return ; + case 'chemical_reaction': return ; + case 'distillation': return ; + case 'arcyne_crafting': return ; + default: + return No details available for type: {r.type}; + } + }; + + return ( + + + {history.length > 0 && ( + + ← {history[history.length - 1].name} + + )} + + + + {r.name} + {r.output_count !== undefined && r.output_count > 1 ? ` ×${r.output_count}` : ''} + + {r.category} + + + {renderBody()} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/RecipeLink.tsx b/tgui/packages/tgui/interfaces/RecipeBook/RecipeLink.tsx new file mode 100644 index 00000000000..b30db1355bb --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/RecipeLink.tsx @@ -0,0 +1,109 @@ +import { useLocalState } from '../../backend'; +import { Box } from 'tgui-core/components'; +import type { Recipe } from './types'; + +export const recipeTypeLabel = (type: string): string => { + const labels: Record = { + repeatable: 'Crafting', brewing: 'Brewing', blueprint: 'Blueprint', + container_craft: 'Cooking', molten: 'Smelting', anvil: 'Smithing', + artificer: 'Artificer', pottery: 'Pottery', runeritual: 'Ritual', + book_entry: 'Lore', alch_cauldron: 'Alchemy', essence_combination: 'Essence', + essence_infusion: 'Infusion', natural_precursor: 'Precursor', + plant_def: 'Farming', surgery: 'Surgery', wound: 'Wound', + chimeric_node: 'Chimeric', chimeric_table: 'Humor', + fish: 'Fish', slapcraft: 'Crafting', orderless_slapcraft: 'Crafting', + snack_processing: 'Processing', obtained_from: 'Source', + source_page: 'Source', organ: 'Organ', chemical_reaction: 'Chemistry', + distillation: 'Distillation', arcyne_crafting: 'Arcyne Crafting', + }; + return labels[type] || type; +}; + +export const RecipePicker = (props: { + name: string; + options: Recipe[]; + onNavigate: (r: Recipe) => void; +}) => { + const { name, options, onNavigate } = props; + const [open, setOpen] = useLocalState(`picker_${name}`, false); + const [coords, setCoords] = useLocalState<{ top: number; left: number } | null>(`picker_coords_${name}`, null); + + const handleClick = (e: any) => { + const rect = e.currentTarget.getBoundingClientRect(); + setCoords({ top: rect.bottom + 2, left: rect.left }); + setOpen(!open); + }; + + return ( + + + {name} ▾ + + {open && coords && ( + + {options.map((r, i) => ( + { setOpen(false); onNavigate(r); }}> + {r.output_name || r.name} + {recipeTypeLabel(r.type)} + + ))} + + )} + + ); +}; + +export const RecipeLink = (props: { + name: string | null | undefined; + path?: string; + lookup: Map; + pickerMap: Map; + allRecipes: Recipe[]; + essenceIndex?: Map; + onNavigate: (r: Recipe) => void; +}) => { + const { name, path, lookup, pickerMap, allRecipes, essenceIndex, onNavigate } = props; + if (!name) return null; + + const pickerByPath = (path && pickerMap.get(path)) || []; + const pickerByName = pickerMap.get(name.toLowerCase()) || []; + const essenceByName = (!path && essenceIndex?.get(name.toLowerCase())) || []; + const merged = [...new Set([...(pickerByPath.length ? pickerByPath : pickerByName), ...essenceByName])]; + + if (merged.length > 1) return ; + if (merged.length === 1 && !path && !lookup.get(name.toLowerCase())) { + const sole = merged[0]; + return onNavigate(sole)} title={`Go to: ${sole.name}`}>{name}; + } + + let target: Recipe | undefined = lookup.get(name.toLowerCase()); + if (!target && path) target = lookup.get(path); + + let subtypeMatches: Recipe[] = []; + if (!target && path) { + const prefix = path + '/'; + subtypeMatches = allRecipes.filter( + (r) => r._output_path && r._output_path.startsWith(prefix) && !r._output_path.substring(prefix.length).includes('/') + ); + if (subtypeMatches.length === 1) target = subtypeMatches[0]; + } + + let essenceMatches: Recipe[] = []; + if (!target && subtypeMatches.length === 0 && essenceIndex && !path) { + essenceMatches = essenceIndex.get(name.toLowerCase()) || []; + if (essenceMatches.length === 1) target = essenceMatches[0]; + } + + const pickerOptions = subtypeMatches.length > 1 ? subtypeMatches : essenceMatches.length > 1 ? essenceMatches : []; + + if (!target && pickerOptions.length === 0) return {name}; + if (!target && pickerOptions.length > 1) return ; + + return ( + onNavigate(target!)} title={`Go to: ${target!.name}`}> + {name} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/Sidebar.tsx b/tgui/packages/tgui/interfaces/RecipeBook/Sidebar.tsx new file mode 100644 index 00000000000..6aca7b7cca5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/Sidebar.tsx @@ -0,0 +1,51 @@ +import { useLocalState } from '../../backend'; +import { Box, Input } from 'tgui-core/components'; +import type { Recipe } from './types'; + +type Props = { + recipes: Recipe[]; + lookup: Map; + selectedRecipe: Recipe | null; + onSelect: (r: Recipe) => void; +}; + +export const Sidebar = ({ recipes, lookup, selectedRecipe, onSelect }: Props) => { + const [search, setSearch] = useLocalState('rb_search', ''); + const [category, setCategory] = useLocalState('rb_category', 'All'); + + const categories = ['All', ...Array.from(new Set(recipes.map((r) => r.category))).sort()]; + + const filtered = recipes.filter((r) => { + const matchCat = category === 'All' || r.category === category; + const q = search.toLowerCase(); + const matchSearch = !q || r.name?.toLowerCase().includes(q) || (r.search_data && r.search_data.toLowerCase().includes(q)); + return matchCat && matchSearch; + }); + + return ( + + + setSearch(value)} className="RecipeBook__search" /> + + + { const el = document.getElementById('rb-cat-strip'); if (el) el.scrollLeft -= 80; }}>← + + {categories.map((cat) => ( + setCategory(cat)}> + {cat} + + ))} + + { const el = document.getElementById('rb-cat-strip'); if (el) el.scrollLeft += 80; }}>→ + + + {!filtered.length && No matching recipes.} + {filtered.map((r, i) => ( + onSelect(r)}> + {r.name} + + ))} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailAlchCauldron.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailAlchCauldron.tsx new file mode 100644 index 00000000000..7a97556e3d2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailAlchCauldron.tsx @@ -0,0 +1,41 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, ItemRow } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailAlchCauldron = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + Requires 50u of Water in cauldron + {!!r.essences?.length && ( + <> + Essences Required + {r.essences!.map((e, i) => ( + + {e.amount} parts{' '} + + + ))} + > + )} + {!!r.output_reagents?.length && ( + <> + Output Reagents + {r.output_reagents!.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + {!!r.output_items?.length && ( + <> + Output Items + {r.output_items!.map((item, i) => ( + + ))} + > + )} + {r.smells_like && 🌿 Smells like: {r.smells_like}} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailAnvil.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailAnvil.tsx new file mode 100644 index 00000000000..622f113dbaa --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailAnvil.tsx @@ -0,0 +1,42 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailAnvil = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + Steps + + + + Place{' '} + + {' '}on anvil + + 🔨 Hammer + {r.extras?.map((item, i) => ( + + + + Add{' '} + + + 🔨 Hammer + + ))} + + {r.output_name && ( + + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailArcyneCrafting.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailArcyneCrafting.tsx new file mode 100644 index 00000000000..f5c1985cc9c --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailArcyneCrafting.tsx @@ -0,0 +1,52 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner } from './../Primitives'; +import { ItemRow } from './../Primitives'; +import type { NavProps } from '../shared'; + +export const DetailArcyneCrafting = ({ + r, lookup, pickerMap, allRecipes, essenceIndex, nav, +}: NavProps) => ( + <> + {r.required_skill !== undefined && r.required_skill > 0 && ( + + ⚑ Required arcane skill: {r.required_skill} + + )} + + Ingredients (order doesn't matter) + {r.ingredients?.map((item, i) => ( + + ))} + + Instructions + + + Draw the Arcyne Crafting Matrix rune with Arcyne Chalk. + + + Place all ingredients on the rune, then invoke it empty-handed. + + + + {r.output_name && ( + + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailArtificer.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailArtificer.tsx new file mode 100644 index 00000000000..4558203caeb --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailArtificer.tsx @@ -0,0 +1,41 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailArtificer = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + Steps + + + + Place{' '} + + {' '}on artificer table + + 🔨 Hammer + {r.extras?.map((item, i) => ( + + + + Add{' '} + + + 🔨 Hammer + + ))} + + {r.output_name && ( + + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBlueprint.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBlueprint.tsx new file mode 100644 index 00000000000..822569b3fb9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBlueprint.tsx @@ -0,0 +1,47 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, ItemRow, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailBlueprint = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {r.desc && } + {!!r.materials?.length && ( + <> + Materials + {r.materials!.map((item, i) => ( + + ))} + > + )} + Construction + + + + Tool:{' '} + + + {r.skill_name && ( + + Skill: {r.skill_name} + {r.skill_diff !== undefined ? ` (diff ${r.skill_diff})` : ''} + + )} + ⏱ {r.build_time}s + {!!r.supports_directions && ↻ Supports rotation} + {!!r.floor_object && ▣ Full floor tile} + + {r.output_name && ( + + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBookEntry.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBookEntry.tsx new file mode 100644 index 00000000000..74f7e73a33f --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBookEntry.tsx @@ -0,0 +1,6 @@ +import { Box } from 'tgui-core/components'; +import type { Recipe } from '../types'; + +export const DetailBookEntry = ({ r }: { r: Recipe }) => ( + +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBrewing.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBrewing.tsx new file mode 100644 index 00000000000..52c9855ff33 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailBrewing.tsx @@ -0,0 +1,75 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, ItemRow, WarnFlag } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailBrewing = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + ⏱ {r.brew_time_s}s brewing time + {r.heat_c !== undefined && ( + Requires heated vessel ≥ {Math.round(r.heat_c!)}C + )} + {r.prereq_name && ( + Requires {r.prereq_name} present in keg + )} + {!!r.ages && ( + Will continue to age after brewing + )} + {r.hints && 💡 {r.hints}} + {!!(r.crops?.length || r.items?.length) && ( + <> + Items Required + {r.crops?.map((item, i) => ( + + ))} + {r.items?.map((item, i) => ( + + ))} + > + )} + {!!r.reagents?.length && ( + <> + Liquids Required + {r.reagents.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + Output + {r.output_liquid && ( + + Liquid + + {r.output_volume} ligulae of {r.output_liquid} + + + )} + {r.output_item_name && ( + + )} + {!!r.age_stages?.length && ( + <> + Aging + {r.age_stages!.map((ag, i) => ( + + After {ag.time_s}s →{' '} + + + ))} + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChemicalReaction.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChemicalReaction.tsx new file mode 100644 index 00000000000..38f56dc10e1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChemicalReaction.tsx @@ -0,0 +1,86 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, WarnFlag } from './../Primitives'; +import { RecipeLink } from './../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailChemicalReaction = ({ + r, lookup, pickerMap, allRecipes, essenceIndex, nav, +}: NavProps) => ( + <> + {!!r.is_cold_recipe && ( + + Cold recipe — react BELOW {r.required_temp ? `${r.required_temp - 273.15}C` : 'required temp'} + + )} + {!r.is_cold_recipe && r.required_temp ? ( + + Requires temperature ≥ {r.required_temp - 273.15}C + + ) : null} + + {r.required_container && ( + + Must react inside: {r.required_container} + + )} + + {r.mob_react === false && ( + + Cannot react inside a living body + + )} + + {!!r.required_reagents?.length && ( + <> + Reagents + {r.required_reagents.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + + {!!r.required_catalysts?.length && ( + <> + Catalysts (not consumed) + {r.required_catalysts.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + + {!!r.results?.length && ( + <> + Output Reagents + {r.results.map((rg, i) => ( + + {rg.amount} ligulae of {rg.name} + + ))} + > + )} + + {r.mix_message && ( + 💬 {r.mix_message} + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChimericNode.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChimericNode.tsx new file mode 100644 index 00000000000..7a32fb4a307 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChimericNode.tsx @@ -0,0 +1,31 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, Badge, WarnFlag } from '../Primitives'; +import type { Recipe } from '../types'; + +export const DetailChimericNode = ({ r }: { r: Recipe }) => ( + <> + {r.desc && } + {r.slot_name} + {!!r.is_special && SPECIAL NODE} + Installation + + {r.allowed_slots?.length ? ( + <> + Can ONLY be installed in: + {r.allowed_slots.map((s, i) => ( + • {s} + ))} + > + ) : r.forbidden_slots?.length ? ( + <> + Cannot be installed in: + {r.forbidden_slots.map((s, i) => ( + • {s} + ))} + > + ) : ( + ✓ Can be installed in any organ + )} + + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChimericTable.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChimericTable.tsx new file mode 100644 index 00000000000..e2d23dd0ff9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailChimericTable.tsx @@ -0,0 +1,84 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; +import type { NodeEntry } from '../types'; + +const NodeList = ({ nodes, color }: { nodes?: NodeEntry[]; color: string }) => { + if (!nodes?.length) return null; + return ( + + {nodes.map((n, i) => ( + + {n.name} + — {n.likelihood} + + ))} + + ); +}; + +export const DetailChimericTable = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + Node Info + + Max tier: {r.node_tier} + + Purity: {r.purity_min}% – {r.purity_max}% (avg {Math.round(((r.purity_min || 0) + (r.purity_max || 0)) / 2)}%) + + + Blood Cost + + Base: {r.base_blood_cost}u/beat + + Preferred: −{((r.pref_bonus || 0) * 100).toFixed(0)}% + + + Incompatible: +{((r.incompat_penalty || 0) * 100).toFixed(0)}% + + + {!!(r.preferred_blood?.length || r.compatible_blood?.length || r.incompatible_blood?.length) && ( + <> + Blood Types + + {r.preferred_blood?.map((b, i) => ( + ★ {b} + ))} + {r.compatible_blood?.map((b, i) => ( + ✓ {b} + ))} + {r.incompatible_blood?.map((b, i) => ( + ✗ {b} + ))} + + > + )} + Input Nodes + + Output Nodes + + Special Nodes + + {!!r.source_mobs?.length && ( + <> + Blood Source Mobs + + {r.source_mobs.map((mob, i) => ( + + + + + ))} + + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailContainerCraft.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailContainerCraft.tsx new file mode 100644 index 00000000000..5985fcadab7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailContainerCraft.tsx @@ -0,0 +1,75 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, ItemRow, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailContainerCraft = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {!!r.requirements?.length && ( + <> + Items + {r.requirements!.map((item, i) => ( + + ))} + > + )} + {!!r.reagents?.length && ( + <> + Liquids + {r.reagents!.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + {!!r.wildcards?.length && ( + <> + Alternative Items + {r.wildcards!.map((wc, i) => ( + + {wc.count}× any {wc.name} + + ))} + > + )} + {r.max_optionals !== undefined && r.max_optionals > 0 && ( + <> + Optional (max {r.max_optionals}) + {r.opt_items?.map((item, i) => ( + + ))} + {r.opt_wildcards?.map((wc, i) => ( + + up to {wc.count}× any {wc.name} + + ))} + > + )} + Process + + + {r.craft_verb} for {r.crafting_time}s + + {r.container_name && ( + + + inside a {r.container_name} + + )} + + {r.extra_html && ( + + )} + {r.output_name && ( + + Creates + + {r.output_count !== undefined && r.output_count > 1 ? `${r.output_count}× ` : ''} + + + + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailDistillation.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailDistillation.tsx new file mode 100644 index 00000000000..9b795447b03 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailDistillation.tsx @@ -0,0 +1,70 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, WarnFlag } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailDistillation = ({ + r, lookup, pickerMap, allRecipes, essenceIndex, nav, +}: NavProps) => ( + <> + + Requires alembic temperature ≥ {r.required_temp ? r.required_temp - 273.15 : '?'}C + + + Primary Input (vaporized) + + + + + {!!r.required_reagents?.length && ( + <> + + Also requires{r.consume_reagents ? ' (consumed)' : ' (not consumed)'} + + {r.required_reagents.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + + {!!r.results?.length && ( + <> + Output (per unit distilled) + {r.results.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + + {r.distill_message && ( + 💬 {r.distill_message} + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailEssenceCombination.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailEssenceCombination.tsx new file mode 100644 index 00000000000..6765d34afdd --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailEssenceCombination.tsx @@ -0,0 +1,34 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailEssenceCombination = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {!!r.inputs?.length && ( + <> + Input Essences + {r.inputs!.map((e, i) => ( + + {e.amount} parts{' '} + + + ))} + > + )} + {r.output_name && ( + + )} + {r.skill_required && typeof r.skill_required === 'string' && ( + Skill required: {r.skill_required} + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailEssenceInfusion.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailEssenceInfusion.tsx new file mode 100644 index 00000000000..c2266a8e2a9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailEssenceInfusion.tsx @@ -0,0 +1,40 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailEssenceInfusion = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + Target Item + + + + + {!!r.essences?.length && ( + <> + Essences + {r.essences!.map((e, i) => ( + + {e.amount} parts{' '} + + + ))} + > + )} + + ⏱ Infusion time: {r.infusion_time}s + + {r.result_name && ( + + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailFish.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailFish.tsx new file mode 100644 index 00000000000..46d206f72fa --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailFish.tsx @@ -0,0 +1,44 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead } from '../Primitives'; +import type { Recipe } from '../types'; + +export const DetailFish = ({ r }: { r: Recipe }) => { + const diffColor = r.difficulty === 'Hard' ? '#d9534f' : r.difficulty === 'Medium' ? '#f0ad4e' : '#5cb85c'; + return ( + <> + {r.desc && } + Physical + + Size: {r.avg_size}cm + Weight: {r.avg_weight}g + + Environment + + Fluid: {r.fluid_type} + Temperature: {r.temp_min}C – {r.temp_max}C + + Fishing + + Found: {r.spots} + + Difficulty: {r.difficulty} + + Favourite bait: {r.fav_bait} + Disliked bait: {r.dislike_bait} + {r.lures?.map((l, i) => ( + • {l} + ))} + + {!!r.traits?.length && ( + <> + Behaviour + + {r.traits!.map((t, i) => ( + • {t} + ))} + + > + )} + > + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailMolten.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailMolten.tsx new file mode 100644 index 00000000000..243e7f8d207 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailMolten.tsx @@ -0,0 +1,32 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailMolten = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + Materials (molten) + {r.materials?.map((m, i) => ( + + {m.count} parts molten{' '} + + + ))} + + + 🌡 Heat to {r.temperature_c !== undefined ? `${Math.round(r.temperature_c!)}C` : '—'} + + + {!!r.outputs?.length && ( + <> + Output + {r.outputs!.map((o, i) => ( + + {o.count} parts{' '} + + + ))} + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailNaturalPrecursor.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailNaturalPrecursor.tsx new file mode 100644 index 00000000000..aa7785318a8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailNaturalPrecursor.tsx @@ -0,0 +1,38 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailNaturalPrecursor = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {!!r.yields?.length && ( + <> + Essence Yields + {r.yields!.map((y, i) => ( + + {y.amount}{' '} + + + ))} + > + )} + {!!r.splits_from?.length && ( + <> + Splits From + {r.splits_from!.map((s, i) => ( + + + + ))} + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailObtainedFrom.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailObtainedFrom.tsx new file mode 100644 index 00000000000..e111112f914 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailObtainedFrom.tsx @@ -0,0 +1,23 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailObtainedFrom = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {!!r.sources?.length && ( + <> + Obtained From + + {r.sources!.map((s, i) => ( + + + + — {s.label} + + ))} + + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailOrderlessSlapcraft.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailOrderlessSlapcraft.tsx new file mode 100644 index 00000000000..55714d4b421 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailOrderlessSlapcraft.tsx @@ -0,0 +1,54 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, HR, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailOrderlessSlapcraft = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => { + return ( + <> + {r.skill_name && ( + With {r.skill_name} skill: + )} + Steps + + + + Start with + + then add: + + {(r.requirements as any[])?.map((req: any, i: number) => { + if (req.choices) { + return ( + + up to {req.count} of: + {req.choices.map((c: any, ci: number) => ( + + + any + + ))} + + + ); + } + return ( + + + + {req.count}× any + + + + ); + })} + {r.finishing_name && ( + + + finish with any + + )} + + > + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailOrgan.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailOrgan.tsx new file mode 100644 index 00000000000..dea17c80db8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailOrgan.tsx @@ -0,0 +1,33 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, ItemRow } from './../Primitives'; +import type { NavProps } from '../shared'; + +export const DetailOrgan = ({ + r, lookup, pickerMap, allRecipes, essenceIndex, nav, +}: NavProps) => ( + <> + {r.zone && Located in: {r.zone}} + + Damage Thresholds + + {r.threshold_low !== undefined && ( + Bruised: {r.threshold_low} + )} + {r.threshold_high !== undefined && ( + Failing: {r.threshold_high} + )} + {r.threshold_max !== undefined && ( + Destroyed: {r.threshold_max} + )} + + + {!!r.healing_items?.length && ( + <> + Healing Items + {r.healing_items.map((item, i) => ( + + ))} + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailPlantDef.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailPlantDef.tsx new file mode 100644 index 00000000000..85c859fd844 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailPlantDef.tsx @@ -0,0 +1,39 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead } from '../Primitives'; +import type { Recipe } from '../types'; + +export const DetailPlantDef = ({ r }: { r: Recipe }) => ( + <> + Growth + + Maturation: {r.maturation_min} min + Produce interval: {r.produce_min} min + Yield: {r.yield_min}–{r.yield_max} + {r.perennial ? '♻ Perennial' : '1× Annual'} + Water drain: {r.water_drain} ligulae/min + {!!r.weed_immune && Weed immune} + {!!r.underground && Can grow underground} + Family: {r.family} + + {!!(r.nitrogen_req || r.phosphorus_req || r.potassium_req) && ( + <> + Nutrient Requirements + + {r.nitrogen_req ? N: {r.nitrogen_req} ligulae : null} + {r.phosphorus_req ? P: {r.phosphorus_req} ligulae : null} + {r.potassium_req ? K: {r.potassium_req} ligulae : null} + + > + )} + {!!(r.nitrogen_prod || r.phosphorus_prod || r.potassium_prod) && ( + <> + Soil Enrichment + + {r.nitrogen_prod ? +N: {r.nitrogen_prod} ligulae : null} + {r.phosphorus_prod ? +P: {r.phosphorus_prod} ligulae : null} + {r.potassium_prod ? +K: {r.potassium_prod} ligulae : null} + + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailPottery.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailPottery.tsx new file mode 100644 index 00000000000..c62841636aa --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailPottery.tsx @@ -0,0 +1,40 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; +import type { PotteryStep } from '../types'; + +export const DetailPottery = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => { + const steps = r.steps as PotteryStep[] | undefined; + return ( + <> + ⚙ Rotational sweetspot: {r.speed_sweetspot} + Steps + + {steps?.map((s, i) => ( + + + + Add{' '} + + {' '}to lathe + + ↻ Spin for {s.time_s}s + + ))} + + {r.output_name && ( + + )} + > + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailRepeatable.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailRepeatable.tsx new file mode 100644 index 00000000000..be2ac13f3f7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailRepeatable.tsx @@ -0,0 +1,69 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, ItemRow, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailRepeatable = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {r.skill_level && ( + + {r.skill_required ? '⚑ Required: ' : '☆ Recommended: '} + {r.skill_name} + + )} + {!!r.requirements?.length && ( + <> + Materials + {r.requirements.map((item, i) => ( + + ))} + > + )} + {!!r.tools?.length && ( + <> + Tools + {r.tools.map((item, i) => ( + + ))} + > + )} + {!!r.reagents?.length && ( + <> + Liquids + {r.reagents.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + Steps + + + + Use {r.starting_name} + + + + on {r.attacked_name} + + {!!r.allow_inverse && ( + or vice versa + )} + + {r.output_name && ( + + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailRuneRitual.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailRuneRitual.tsx new file mode 100644 index 00000000000..5caaea6287e --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailRuneRitual.tsx @@ -0,0 +1,23 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, ItemRow, Badge } from '../Primitives'; +import type { NavProps } from '../shared'; + +export const DetailRuneRitual = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + Complexity Tier {r.tier} + {!!r.items?.length && ( + <> + Items Required + {r.items!.map((item, i) => ( + + ))} + > + )} + Instructions + + + Draw the required rune with Arcyne Chalk, then supply the above items. + + + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSlapcraft.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSlapcraft.tsx new file mode 100644 index 00000000000..4199a6b9db4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSlapcraft.tsx @@ -0,0 +1,79 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, OutputBanner, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; +import type { SlapcraftStep } from '../types'; + +export const DetailSlapcraft = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => { + const steps = r.steps as SlapcraftStep[] | undefined; + if (!steps) return null; + const first = steps[0]; + const second = steps[1]; + return ( + <> + Steps + + {second && (() => { + const parts = second.desc.split(second.name); + return ( + + + {parts.length > 1 ? ( + <> + {parts[0]} + + {parts.slice(1).join(second.name)} + > + ) : ( + <> + + {' '}{second.verb} + > + )} + + ); + })()} + {first && ( + + + a{' '} + + + )} + {steps.slice(2).map((s, i) => { + const descParts = s.desc.split(s.name); + return ( + + + {descParts.length > 1 ? ( + <> + {descParts[0]} + + {descParts.slice(1).join(s.name)} + > + ) : ( + <> + {s.desc}{' '} + + > + )} + {!!s.optional && (optional)} + + ); + })} + + {r.output_name && ( + + )} + > + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSnackProcessing.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSnackProcessing.tsx new file mode 100644 index 00000000000..4c277dc5e05 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSnackProcessing.tsx @@ -0,0 +1,87 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, ItemRow, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailSnackProcessing = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {r.mill_name && ( + <> + Milling + + Mills into + + + + + + > + )} + {!!r.milled_from?.length && ( + <> + Milled From + {r.milled_from!.map((item, i) => ( + + ))} + > + )} + {!!r.sliced_from?.length && ( + <> + Sliced From + {r.sliced_from!.map((item, i) => ( + + ))} + > + )} + {!!r.sources?.length && ( + <> + Obtained From + + {r.sources!.map((s, i) => ( + + + + — {s.label} + + ))} + + > + )} + {!!r.grind_results?.length && ( + <> + Grinding + {r.grind_results!.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + {!!r.juice_results?.length && ( + <> + Juicing + {r.juice_results!.map((rg, i) => ( + + {rg.amount} ligulae of{' '} + + + ))} + > + )} + {r.slice_name && ( + <> + Slicing + + Slices into + + + {r.slice_num !== undefined && r.slice_num > 1 ? `${r.slice_num}× ` : ''} + + {r.slice_skill && — requires {r.slice_skill}} + + + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSourcePage.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSourcePage.tsx new file mode 100644 index 00000000000..b486a21cc57 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSourcePage.tsx @@ -0,0 +1,23 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, Sprite } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; + +export const DetailSourcePage = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => ( + <> + {!!r.drops?.length && ( + <> + Drops + + {r.drops!.map((d, i) => ( + + + + {d.source_label && — {d.source_label}} + + ))} + + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSurgery.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSurgery.tsx new file mode 100644 index 00000000000..a864d564a68 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSurgery.tsx @@ -0,0 +1,62 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, WarnFlag, HR } from '../Primitives'; +import { RecipeLink } from '../RecipeLink'; +import type { NavProps } from '../shared'; +import type { SurgeryStep } from '../types'; + +export const DetailSurgery = ({ r, lookup, pickerMap, allRecipes, essenceIndex, nav }: NavProps) => { + const steps = r.steps as SurgeryStep[] | undefined; + return ( + <> + {!!r.heretical && HERETICAL RESEARCH} + {r.desc && } + {!!r.req_bodypart && Requires bodypart to be present} + {!!r.req_missing_bodypart && Requires bodypart to be MISSING} + {!!r.req_real_bodypart && Cannot be performed on prosthetics} + Procedure + {steps?.map((s, i) => ( + + Step {i + 1}: {s.name} + {s.desc && } + {!!s.tools?.length && ( + + Tools: + {s.tools!.map((t, ti) => ( + + + {' '}({t.chance}%) + + ))} + + )} + {s.skill_name && ( + + Min: / Optimal:{' '} + {s.skill_name} + + )} + {s.chems && ( + Chemicals: {s.chems} + )} + {!!s.flags?.length && ( + + {s.flags!.map((f, fi) => ( + • {f} + ))} + + )} + {(!!s.accept_hand || !!s.accept_any || !s.self_operable || !!s.lying_required || !!s.repeating) && ( + + {!!s.accept_hand && Can use bare hands} + {!!s.accept_any && Accepts any item} + {!s.self_operable && Cannot self-operate} + {!!s.lying_required && Patient must be lying down} + {!!s.repeating && Repeatable until failure} + + )} + + + ))} + > + ); +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailWound.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailWound.tsx new file mode 100644 index 00000000000..793468000e0 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailWound.tsx @@ -0,0 +1,82 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead, WarnFlag } from '../Primitives'; +import type { Recipe } from '../types'; + +export const DetailWound = ({ r }: { r: Recipe }) => ( + <> + {r.desc && } + + Severity: {r.severity_text} + + {!!r.critical && CRITICAL WOUND} + {!!r.mortal && MORTAL WOUND} + {!!r.disabling && DISABLING WOUND} + Wound Stats + + WHP: {r.whp} + {r.passive_healing !== undefined && ( + Passive healing: {r.passive_healing}/beat + )} + {r.sleep_healing !== undefined && ( + Sleep healing: {r.sleep_healing}/beat + )} + + {(r.can_sew || r.can_cauterize) && ( + <> + Treatment + + {!!r.can_sew && ( + ✂ Sewable ({r.sew_threshold} progress → {r.sewn_whp} WHP) + )} + {!!r.can_cauterize && ( + 🔥 Can be cauterized + )} + + > + )} + {r.bleed_rate !== undefined && ( + <> + Bleeding + + Rate: {r.bleed_rate} + {r.sewn_bleed_rate !== undefined && ( + Rate (sewn): {r.sewn_bleed_rate} + )} + {r.clotting_rate && ( + + Clotting: {r.clotting_rate}/beat{r.clotting_threshold !== undefined ? ` → ${r.clotting_threshold}` : ''} + + )} + + > + )} + {r.woundpain !== undefined && ( + <> + Pain + + + Pain: {r.woundpain}{r.sewn_woundpain !== undefined ? ` (sewn: ${r.sewn_woundpain})` : ''} + + + > + )} + {!!r.special_props?.length && ( + <> + Special Properties + + {r.special_props!.map((sp, i) => ( + • {sp} + ))} + + > + )} + {r.check_name && ( + <> + Diagnosis + + + + > + )} + > +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/shared.ts b/tgui/packages/tgui/interfaces/RecipeBook/shared.ts new file mode 100644 index 00000000000..5b18d7ec401 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/shared.ts @@ -0,0 +1,10 @@ +import type { Recipe } from './types'; + +export type NavProps = { + r: Recipe; + lookup: Map; + pickerMap: Map; + allRecipes: Recipe[]; + essenceIndex: Map; + nav: (r: Recipe) => void; +}; diff --git a/tgui/packages/tgui/interfaces/RecipeBook/types.ts b/tgui/packages/tgui/interfaces/RecipeBook/types.ts new file mode 100644 index 00000000000..c024daf6480 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/types.ts @@ -0,0 +1,281 @@ +export interface IconData { + icon: string; + icon_state: string; +} + +export interface ItemRef extends IconData { + name: string; + count?: number; + any?: boolean; + _path?: string; +} + +export interface ReagentRef { + name: string; + amount: number; +} + +export interface SurgeryStep { + name: string; + desc?: string; + tools?: { name: string; chance: number }[]; + accept_hand?: boolean; + accept_any?: boolean; + self_operable?: boolean; + lying_required?: boolean; + repeating?: boolean; + ignore_clothes?: boolean; + skill_name?: string; + skill_min?: string; + skill_median?: string; + chems?: string; + organs?: string[]; + flags?: string[]; +} + +export interface PotteryStep extends IconData { + name: string; + time_s: number; +} + +export interface SlapcraftStep extends IconData { + name: string; + desc: string; + optional: boolean; + verb: string; + index: number; + recipe_link?: string; + _path?: string; +} + +export interface NodeEntry { + name: string; + likelihood: string; +} + +export interface AgeStage { + name: string; + time_s: number; +} + +export interface EssenceEntry { + name: string; + amount: number; +} + +export interface Recipe { + type: string; + name: string; + category: string; + output_name?: string; + output_icon?: string; + output_state?: string; + output_count?: number; + _output_path?: string; + _extra_output_paths?: string[]; + yield_names?: string[]; + requirements?: ItemRef[]; + tools?: ItemRef[]; + reagents?: ReagentRef[]; + skill_name?: string; + skill_level?: string; + skill_required?: boolean | string; + starting_name?: string; + starting_icon?: string; + starting_state?: string; + attacked_name?: string; + attacked_icon?: string; + attacked_state?: string; + allow_inverse?: boolean; + steps?: (SlapcraftStep | PotteryStep | SurgeryStep)[]; + finishing_name?: string; + finishing_icon?: string; + finishing_state?: string; + desc?: string; + materials?: ItemRef[]; + tool_name?: string; + tool_icon?: string; + tool_state?: string; + _tool_path?: string; + skill_diff?: number; + build_time?: number; + supports_directions?: boolean; + floor_object?: boolean; + craft_verb?: string; + crafting_time?: number; + wildcards?: { name: string; count: number }[]; + max_optionals?: number; + opt_items?: ItemRef[]; + opt_wildcards?: { name: string; count: number }[]; + container_name?: string; + container_icon?: string; + container_state?: string; + extra_html?: string; + temperature_c?: number; + outputs?: { name: string; count: number }[]; + bar_name?: string; + bar_icon?: string; + bar_state?: string; + base_name?: string; + base_icon?: string; + base_state?: string; + extras?: ItemRef[]; + brew_time_s?: number; + hints?: string; + heat_c?: number; + prereq_name?: string; + ages?: boolean; + crops?: ItemRef[]; + items?: ItemRef[]; + output_liquid?: string; + output_volume?: number; + output_item_name?: string; + output_item_icon?: string; + output_item_state?: string; + output_item_count?: number; + age_stages?: AgeStage[]; + tier?: number; + html?: string; + essences?: EssenceEntry[]; + output_reagents?: { name: string; amount: number }[]; + output_items?: ItemRef[]; + smells_like?: string; + inputs?: EssenceEntry[]; + output_amount?: number; + target_name?: string; + target_icon?: string; + target_state?: string; + result_name?: string; + result_icon?: string; + result_state?: string; + infusion_time?: number; + yields?: EssenceEntry[]; + splits_from?: string[]; + splits_from_paths?: string[]; + search_data?: string; + maturation_min?: number; + produce_min?: number; + yield_min?: number; + yield_max?: number; + perennial?: boolean; + water_drain?: number; + weed_immune?: boolean; + underground?: boolean; + family?: string; + nitrogen_req?: number; + phosphorus_req?: number; + potassium_req?: number; + nitrogen_prod?: number; + phosphorus_prod?: number; + potassium_prod?: number; + heretical?: boolean; + req_bodypart?: boolean; + req_missing_bodypart?: boolean; + req_real_bodypart?: boolean; + severity_text?: string; + severity_color?: string; + critical?: boolean; + mortal?: boolean; + disabling?: boolean; + whp?: number; + can_sew?: boolean; + can_cauterize?: boolean; + sew_threshold?: number; + sewn_whp?: number; + bleed_rate?: number; + sewn_bleed_rate?: number; + clotting_rate?: number; + clotting_threshold?: number; + sewn_clotting_rate?: number; + sewn_clotting_threshold?: number; + passive_healing?: number; + sleep_healing?: number; + woundpain?: number; + sewn_woundpain?: number; + special_props?: string[]; + check_name?: string; + slot_name?: string; + slot_color?: string; + is_special?: boolean; + allowed_slots?: string[]; + forbidden_slots?: string[]; + node_tier?: number; + purity_min?: number; + purity_max?: number; + base_blood_cost?: number; + pref_bonus?: number; + incompat_penalty?: number; + preferred_blood?: string[]; + compatible_blood?: string[]; + incompatible_blood?: string[]; + input_nodes?: NodeEntry[]; + output_nodes?: NodeEntry[]; + special_nodes?: NodeEntry[]; + source_mobs?: { name: string; icon: string; icon_state: string; _path: string }[]; + mill_name?: string; + mill_icon?: string; + mill_state?: string; + mill_path?: string; + grind_results?: { name: string; amount: number }[]; + juice_results?: { name: string; amount: number }[]; + milled_from?: ItemRef[]; + sliced_from?: ItemRef[]; + slice_name?: string; + slice_icon?: string; + slice_state?: string; + slice_path?: string; + slice_num?: number; + slice_skill?: string; + avg_size?: number; + avg_weight?: number; + fluid_type?: string; + temp_min?: number; + temp_max?: number; + spots?: string; + difficulty?: string; + fav_bait?: string; + dislike_bait?: string; + lures?: string[]; + traits?: string[]; + sources?: { label: string; _path: string; name: string; icon: string; icon_state: string }[]; + drops?: { name: string; icon: string; icon_state: string; _path: string; source_label: string }[]; + speed_sweetspot?: string | number; + zone?: string; + threshold_low?: number; + threshold_high?: number; + threshold_max?: number; + msg_bruised?: string; + msg_broken?: string; + msg_bruised_healed?: string; + msg_broken_healed?: string; + msg_failing?: string; + msg_fixed?: string; + healing_factor?: number; + healing_items?: ItemRef[]; + healing_tools?: string[]; + attaching_items?: ItemRef[]; + blood_req?: number; + oxygen_req?: number; + nutriment_req?: number; + hydration_req?: number; + required_reagents?: ReagentRef[]; + required_catalysts?: ReagentRef[]; + is_cold_recipe?: number; + mob_react?: boolean; + required_container?: string; + mix_message?: string; + required_temp?: number; + results?: { name: string; amount: number }[]; + distilled_reagent_name?: string; + consume_reagents?: boolean; + distill_message?: string; + ingredients?: ItemRef[]; + required_skill?: number; +} + +export interface RecipeBookData { + book_name: string; + book_desc: string; + recipes: Recipe[]; + linked_recipes: Recipe[]; +} diff --git a/vanderlin.dme b/vanderlin.dme index d24a3ddfeb6..b2ac9f4ed07 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -368,6 +368,7 @@ #include "code\_globalvars\lists\flavor_misc.dm" #include "code\_globalvars\lists\icons.dm" #include "code\_globalvars\lists\keybindings.dm" +#include "code\_globalvars\lists\magic.dm" #include "code\_globalvars\lists\mapping.dm" #include "code\_globalvars\lists\mobs.dm" #include "code\_globalvars\lists\names.dm" @@ -1236,6 +1237,7 @@ #include "code\datums\enchantments\pestra_gift.dm" #include "code\datums\enchantments\phoenix_guard.dm" #include "code\datums\enchantments\pocket_dimension.dm" +#include "code\datums\enchantments\pylon_rider.dm" #include "code\datums\enchantments\shattering.dm" #include "code\datums\enchantments\shrinking.dm" #include "code\datums\enchantments\silver.dm" @@ -1442,8 +1444,30 @@ #include "code\datums\quirks\vices\special.dm" #include "code\datums\rage\_base.dm" #include "code\datums\rage\werewolf_rage.dm" -#include "code\datums\rituals\ritual.dm" -#include "code\datums\rituals\ritual_runes.dm" +#include "code\datums\rituals\runes\_base.dm" +#include "code\datums\rituals\runes\arcane\attunement.dm" +#include "code\datums\rituals\runes\arcane\crafting.dm" +#include "code\datums\rituals\runes\arcane\deconstruction.dm" +#include "code\datums\rituals\runes\arcane\empowerment.dm" +#include "code\datums\rituals\runes\arcane\etching.dm" +#include "code\datums\rituals\runes\arcane\greater_wall.dm" +#include "code\datums\rituals\runes\arcane\knowledge.dm" +#include "code\datums\rituals\runes\arcane\leylines.dm" +#include "code\datums\rituals\runes\arcane\spell_imbuement.dm" +#include "code\datums\rituals\runes\arcane\summoning.dm" +#include "code\datums\rituals\runes\arcane\teleport.dm" +#include "code\datums\rituals\runes\arcane\wall.dm" +#include "code\datums\rituals\runes\arcane\draining\_base.dm" +#include "code\datums\rituals\runes\arcane\draining\action_speed.dm" +#include "code\datums\rituals\runes\arcane\draining\cooking.dm" +#include "code\datums\rituals\runes\arcane\draining\experience.dm" +#include "code\datums\rituals\runes\arcane\draining\flight.dm" +#include "code\datums\rituals\runes\arcane\draining\plant_growth.dm" +#include "code\datums\rituals\runes\arcane\draining\waterwheel.dm" +#include "code\datums\rituals\runes\rituals\_base.dm" +#include "code\datums\rituals\runes\rituals\buffs.dm" +#include "code\datums\rituals\runes\rituals\summoning.dm" +#include "code\datums\rituals\runes\rituals\walls.dm" #include "code\datums\rts\controller_mob.dm" #include "code\datums\rts\stockpile_datum.dm" #include "code\datums\rts\work_mind.dm" @@ -1824,6 +1848,7 @@ #include "code\game\objects\effects\temporary_visuals\temporary_visual.dm" #include "code\game\objects\effects\temporary_visuals\projectiles\projectile_effects.dm" #include "code\game\objects\effects\temporary_visuals\projectiles\tracer.dm" +#include "code\game\objects\items\arcyne_spellobject.dm" #include "code\game\objects\items\augment.dm" #include "code\game\objects\items\bags.dm" #include "code\game\objects\items\bait.dm" @@ -1882,12 +1907,14 @@ #include "code\game\objects\items\servant_bell.dm" #include "code\game\objects\items\signal_horn.dm" #include "code\game\objects\items\soap.dm" +#include "code\game\objects\items\spell_focus.dm" #include "code\game\objects\items\spellbook.dm" #include "code\game\objects\items\statue.dm" #include "code\game\objects\items\tent_kit.dm" #include "code\game\objects\items\toys.dm" #include "code\game\objects\items\trash.dm" #include "code\game\objects\items\undies.dm" +#include "code\game\objects\items\unfiinshed_spellbook.dm" #include "code\game\objects\items\waterskins.dm" #include "code\game\objects\items\weaponry.dm" #include "code\game\objects\items\chimeric_organs\chimeric_node.dm" @@ -2738,6 +2765,7 @@ #include "code\modules\crafting\quality_of_crafting\_base.dm" #include "code\modules\crafting\quality_of_crafting\alchemy.dm" #include "code\modules\crafting\quality_of_crafting\arcyne.dm" +#include "code\modules\crafting\quality_of_crafting\arcyne_crafting.dm" #include "code\modules\crafting\quality_of_crafting\bombs.dm" #include "code\modules\crafting\quality_of_crafting\books.dm" #include "code\modules\crafting\quality_of_crafting\crafted_items.dm"