diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 6789e80f924..c729007bbcc 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -77,6 +77,7 @@ var/list/admin_verbs_admin = list( /client/proc/toggle_advanced_interaction, /*toggle admin ability to interact with not only machines, but also atoms such as buttons and doors*/ /client/proc/list_ssds, /client/proc/cmd_admin_headset_message, + /client/proc/spawn_floor_cluwne, ) var/list/admin_verbs_ban = list( /client/proc/unban_panel, diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 3814bc45225..841cfb69036 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1853,6 +1853,7 @@ ptypes += "Assassin" ptypes += "Hunter" ptypes += "Crew Traitor" + ptypes += "Floor Cluwne" var/punishment = input(owner, "How would you like to smite [M]?", "Its good to be baaaad...", "") as null|anything in ptypes if(!(punishment in ptypes)) return @@ -1960,6 +1961,12 @@ if("Gib") logmsg = "gibbed." M.gib(FALSE) + if("Floor Cluwne") + logmsg = "floor cluwne" + var/turf/T = get_turf(M) + var/mob/living/simple_animal/hostile/floor_cluwne/FC = new /mob/living/simple_animal/hostile/floor_cluwne(T) + FC.smiting = TRUE + FC.Acquire_Victim(M) if(logmsg) log_admin("[key_name(owner)] answered [key_name(M)]'s prayer with a smiting: [logmsg]") message_admins("[key_name_admin(owner)] answered [key_name_admin(M)]'s prayer with a smiting: [logmsg]") diff --git a/code/modules/admin/verbs/spawnfloorcluwne.dm b/code/modules/admin/verbs/spawnfloorcluwne.dm new file mode 100644 index 00000000000..02ff9eda6e2 --- /dev/null +++ b/code/modules/admin/verbs/spawnfloorcluwne.dm @@ -0,0 +1,26 @@ +/client/proc/spawn_floor_cluwne() + set category = "Event" + set name = "Unleash Floor Cluwne" + set desc = "Pick a specific target or just let it select randomly and spawn the floor cluwne mob on the station. Be warned: spawning more than one may cause issues!" + var/target + + if(!check_rights(R_EVENT)) + return + + var/confirm = alert("Are you sure you want to release a floor cluwne and kill alot of people?", "Confirm Massacre", "Yes", "No") + if(confirm == "Yes") + + var/turf/T = get_turf(usr) + target = input("Any specific target in mind? Please note only live, non cluwned, human targets are valid.", "Target", target) as null|anything in GLOB.player_list + if(isnull(target)) + return + if(target && ishuman(target)) + var/mob/living/carbon/human/H = target + var/mob/living/simple_animal/hostile/floor_cluwne/FC = new /mob/living/simple_animal/hostile/floor_cluwne(T) + FC.Acquire_Victim(H) + else + new /mob/living/simple_animal/hostile/floor_cluwne(T) + log_admin("[key_name(usr)] spawned floor cluwne.") + message_admins("[key_name(usr)] spawned floor cluwne.") + else + return diff --git a/code/modules/events/event_container.dm b/code/modules/events/event_container.dm index 0a99ed6fd14..c90cbd12777 100644 --- a/code/modules/events/event_container.dm +++ b/code/modules/events/event_container.dm @@ -195,7 +195,8 @@ var/list/event_last_fired = list() new /datum/event_meta/alien(EVENT_LEVEL_MAJOR, "Alien Infestation", /datum/event/alien_infestation, 0, list(ASSIGNMENT_SECURITY = 15), 1), new /datum/event_meta(EVENT_LEVEL_MAJOR, "Traders", /datum/event/traders, 0, is_one_shot = 1), new /datum/event_meta(EVENT_LEVEL_MAJOR, "Terror Spiders", /datum/event/spider_terror, 0, list(ASSIGNMENT_SECURITY = 15), is_one_shot = 1), - new /datum/event_meta(EVENT_LEVEL_MAJOR, "Slaughter Demon", /datum/event/spawn_slaughter, 15, is_one_shot = 1) + new /datum/event_meta(EVENT_LEVEL_MAJOR, "Slaughter Demon", /datum/event/spawn_slaughter, 15, is_one_shot = 1), + new /datum/event_meta(EVENT_LEVEL_MAJOR, "Floor Cluwne", /datum/event/spawn_floor_cluwne, 15, is_one_shot = 1) ) diff --git a/code/modules/events/floorcluwne.dm b/code/modules/events/floorcluwne.dm new file mode 100644 index 00000000000..12c2c4aea95 --- /dev/null +++ b/code/modules/events/floorcluwne.dm @@ -0,0 +1,15 @@ +/datum/event/spawn_floor_cluwne + + +/datum/event/spawn_floor_cluwne/start() + + if(!xeno_spawn) + message_admins("No valid spawn locations found, aborting...") + return kill() + + var/turf/T = get_turf(pick(xeno_spawn)) + var/mob/living/simple_animal/hostile/floor_cluwne/S = new(T) + playsound(S, 'sound/spookoween/scary_horn.ogg', 50, 1, -1) + message_admins("A floor cluwne has been spawned at [COORD(T)][ADMIN_JMP(T)]") + log_game("A floor cluwne has been spawned at [COORD(T)]") + return 1 \ No newline at end of file diff --git a/code/modules/mob/living/simple_animal/hostile/floorcluwne.dm b/code/modules/mob/living/simple_animal/hostile/floorcluwne.dm new file mode 100644 index 00000000000..76f08cdd409 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/floorcluwne.dm @@ -0,0 +1,422 @@ +#define STAGE_HAUNT 1 +#define STAGE_SPOOK 2 +#define STAGE_TORMENT 3 +#define STAGE_ATTACK 4 +#define MANIFEST_DELAY 9 + +/mob/living/simple_animal/hostile/floor_cluwne + name = "???" + desc = "...." + icon = 'icons/goonstation/objects/clothing/mask.dmi' + icon_state = "cursedclown" + icon_living = "cursedclown" + icon_gib = "clown_gib" + maxHealth = 200 + health = 200 + speed = -1 + attacktext = "attacks" + attack_sound = 'sound/items/bikehorn.ogg' + del_on_death = TRUE + pass_flags = PASSTABLE | PASSGRILLE | PASSMOB | LETPASSTHROW | PASSGLASS | PASSBLOB//it's practically a ghost when unmanifested (under the floor) + loot = list(/obj/item/clothing/mask/cursedclown) + a_intent = INTENT_HARM + wander = FALSE + minimum_distance = 2 + move_to_delay = 1 + environment_smash = FALSE + pixel_y = 8 + pressure_resistance = 200 + minbodytemp = 0 + maxbodytemp = 1500 + atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) + var/mob/living/carbon/human/current_victim + var/manifested = FALSE + var/switch_stage = 60 + var/stage = STAGE_HAUNT + var/interest = 0 + var/target_area + var/invalid_area_typecache = list(/area/space, /area/centcom) + var/eating = FALSE + var/smiting = FALSE + var/admincluwne = FALSE + + +/mob/living/simple_animal/hostile/floor_cluwne/New() + . = ..() + remove_from_all_data_huds() + var/obj/item/card/id/access_card = new (src) + access_card.access = get_all_accesses()//THERE IS NO ESCAPE + access_card.flags |= NODROP + invalid_area_typecache = typecacheof(invalid_area_typecache) + Manifest() + if(!current_victim) + var/turf/current_location = get_turf(src) + if(!is_admin_level(current_location.z))//admin spawned off station Z for some reason do not trigger anything else + admincluwne = TRUE + return + Acquire_Victim() + + +/mob/living/simple_animal/hostile/floor_cluwne/Destroy() + return ..() + + +/mob/living/simple_animal/hostile/floor_cluwne/attack_hand(mob/living/carbon/human/M) + ..() + playsound(src.loc, 'sound/items/bikehorn.ogg', 50, 1) + + +/mob/living/simple_animal/hostile/floor_cluwne/CanPass(atom/A, turf/target) + return TRUE + + +/mob/living/simple_animal/hostile/floor_cluwne/Life(seconds, times_fired) + do_jitter_animation(1000) + pixel_y = 8 + + if(is_type_in_typecache(get_area(src.loc), invalid_area_typecache)) + var/area = pick(teleportlocs) + var/area/tp = teleportlocs[area] + forceMove(pick(get_area_turfs(tp.type))) + + if(!current_victim && !admincluwne) + Acquire_Victim() + + if(stage && !manifested) + On_Stage() + + if(stage == STAGE_ATTACK) + playsound(src, 'sound/spookoween/ghost_whisper.ogg', 75, 1) + + if(eating) + return + + var/turf/T = get_turf(current_victim) + if(prob(5))//checks roughly every 20 ticks + if(!admincluwne) + if(current_victim.stat == DEAD || current_victim.get_int_organ(/obj/item/organ/internal/honktumor/cursed) || is_type_in_typecache(get_area(T), invalid_area_typecache)) + Acquire_Victim() + + if(get_dist(src, current_victim) > 9 && !manifested && !is_type_in_typecache(get_area(T), invalid_area_typecache))//if cluwne gets stuck he just teleports + do_teleport(src, T) + + interest++ + if(interest >= switch_stage * 4) + stage = STAGE_ATTACK + + else if(interest >= switch_stage * 2) + stage = STAGE_TORMENT + + else if(interest >= switch_stage) + stage = STAGE_SPOOK + + else if(interest < switch_stage) + stage = STAGE_HAUNT + + ..() + + +/mob/living/simple_animal/hostile/floor_cluwne/Goto(target, delay, minimum_distance) + if(!manifested && !is_type_in_typecache(get_area(current_victim.loc), invalid_area_typecache)) + walk_to(src, target, minimum_distance, delay) + else + walk_to(src,0) + + +/mob/living/simple_animal/hostile/floor_cluwne/FindTarget() + return current_victim + + +/mob/living/simple_animal/hostile/floor_cluwne/CanAttack(atom/the_target)//you will not escape + return TRUE + + +/mob/living/simple_animal/hostile/floor_cluwne/AttackingTarget() + return + + +/mob/living/simple_animal/hostile/floor_cluwne/LoseTarget() + return + + +/mob/living/simple_animal/hostile/floor_cluwne/electrocute_act(shock_damage, obj/source, siemens_coeff = 1, safety = 0, tesla_shock = 0, illusion = 0, stun = TRUE)//prevents runtimes with machine fuckery + return FALSE + + +/mob/living/simple_animal/hostile/floor_cluwne/proc/Acquire_Victim(specific) + var/list/players_copy = GLOB.player_list.Copy() + while(players_copy.len) + var/mob/living/carbon/human/H = pick_n_take(players_copy) + + if(specific) + H = specific + if((!H || H.stat == DEAD) && smiting)//safety check, target somehow DIED after we sent a smite + message_admins("Smiting Floor Cluwne was deleted due to a lack of valid target. Someone killed them first.") + qdel(src) + if(H.stat != DEAD && !isLivingSSD(H) && H.client && !H.get_int_organ(/obj/item/organ/internal/honktumor/cursed) && !is_type_in_typecache(get_area(H.loc), invalid_area_typecache)) + current_victim = H + return target = current_victim + + if(H && ishuman(H) && H.stat != DEAD && H != current_victim && !isLivingSSD(H) && H.client && !H.get_int_organ(/obj/item/organ/internal/honktumor/cursed) && !is_type_in_typecache(get_area(H.loc), invalid_area_typecache)) + current_victim = H + interest = 0 + return target = current_victim + + + message_admins("Floor Cluwne was deleted due to a lack of valid targets, if this was a manually targeted instance please re-evaluate your choice.") + qdel(src) + +/mob/living/simple_animal/hostile/floor_cluwne/proc/Manifest()//handles disappearing and appearance anim + if(manifested) + new /obj/effect/temp_visual/fcluwne_manifest(loc) + addtimer(CALLBACK(src, /mob/living/simple_animal/hostile/floor_cluwne/.proc/Appear), MANIFEST_DELAY) + + else + layer = GAME_PLANE + invisibility = INVISIBILITY_MAXIMUM + mouse_opacity = 0 + density = FALSE + +/mob/living/simple_animal/hostile/floor_cluwne/proc/Appear()//handled in a seperate proc so floor cluwne doesn't appear before the animation finishes + layer = MOB_LAYER + invisibility = FALSE + mouse_opacity = 1 + density = TRUE + + +/mob/living/simple_animal/hostile/floor_cluwne/proc/Reset_View(screens, color, mob/living/carbon/human/H) + if(screens) + for(var/whole_screen in screens) + animate(whole_screen, transform = matrix(), time = 5, easing = QUAD_EASING) + if(color && H) + animate(H.client, color = color, time = 5) + + +/mob/living/simple_animal/hostile/floor_cluwne/proc/On_Stage() + var/mob/living/carbon/human/H = current_victim + switch(stage) + + if(STAGE_HAUNT) + + if(prob(5)) + H.AdjustEyeBlurry(1) + + if(prob(5)) + H.playsound_local(src,'sound/spookoween/insane_low_laugh.ogg', 1) + + if(prob(8)) + H.playsound_local(src,'sound/spookoween/ghost_whisper.ogg', 5) + + if(prob(5)) + var/obj/item/I = locate() in orange(H, 8) + if(I && !I.anchored) + I.throw_at(H, 4, 3) + to_chat(H, "What threw that?") + + if(STAGE_SPOOK) + + if(prob(4)) + H.slip("???", 5, 2) + to_chat(H, "The floor shifts underneath you!") + + if(prob(3)) + H.playsound_local(src,'sound/spookoween/scary_horn.ogg', 2) + + if(prob(8)) + H.playsound_local(src,'sound/spookoween/scary_horn2.ogg', 2) + + if(prob(5)) + H.playsound_local(src,'sound/hallucinations/growl1.ogg', 10) + to_chat(H, "knoh") + + if(prob(5)) + var/obj/item/I = locate() in orange(H, 8) + if(I && !I.anchored) + I.throw_at(H, 4, 3) + to_chat(H, "What threw that?") + + if(prob(4)) + to_chat(H, "yalp ot tnaw I") + Appear() + manifested = FALSE + addtimer(CALLBACK(src, /mob/living/simple_animal/hostile/floor_cluwne/.proc/Manifest), 1) + + if(STAGE_TORMENT) + + if(prob(5)) + H.slip("???", 5, 2) + to_chat(H, "The floor shifts underneath you!") + + if(prob(5)) + playsound(src, pick('sound/spookoween/scary_horn.ogg', 'sound/spookoween/scary_horn2.ogg', 'sound/spookoween/scary_horn3.ogg'), 30, 1) + + if(prob(3)) + playsound(src, pick('sound/hallucinations/growl1.ogg', 'sound/hallucinations/growl2.ogg') , 30, 1) + + + if(prob(4)) + for(var/obj/item/I in orange(H, 5)) + if(I && !I.anchored) + I.throw_at(H, 4, 3) + to_chat(H, "What the hell?!") + + if(prob(5)) + to_chat(H, "Something feels very wrong...") + H.playsound_local(src,'sound/hallucinations/behind_you1.ogg', 25) + H.flash_eyes() + + if(prob(5)) + to_chat(H, "!?REHTOMKNOH eht esiarp uoy oD") + to_chat(H, "Something grabs your foot!") + H.playsound_local(src,'sound/hallucinations/i_see_you1.ogg', 25) + H.Stun(10) + + if(prob(5)) + to_chat(H, "!KNOH ?od nottub siht seod tahW") + for(var/obj/machinery/M in range(H, 6)) + M.attack_hand(src) + + if(prob(6)) + for(var/turf/simulated/floor/O in range(src, 6)) + O.MakeSlippery(TURF_WET_WATER, 10) + playsound(src, 'sound/effects/clownstep1.ogg', 30, 1) + + if(prob(5)) + to_chat(H, "WHAT THE FUCK IS THAT?!") + to_chat(H, ".KNOH !nuf hcum os si uoy htiw gniyalP .KNOH KNOH KNOH") + H.playsound_local(src,'sound/hallucinations/im_here1.ogg', 25) + H.reagents.add_reagent("lsd", 3) + Appear() + manifested = FALSE + addtimer(CALLBACK(src, /mob/living/simple_animal/hostile/floor_cluwne/.proc/Manifest), 2) + for(var/obj/machinery/light/L in range(H, 8)) + L.flicker() + + if(STAGE_ATTACK) + + if(!eating) + for(var/I in getline(src,H)) + var/turf/T = I + for(var/obj/structure/O in T) + if(istype(O, /obj/structure/closet)) + var/obj/structure/closet/locker = O + locker.dump_contents() + if(O.density || istype(O, /obj/machinery/door/airlock)) + forceMove(H.loc) + if(T.density) + forceMove(H.loc) + if(H.buckled) + H.buckled.unbuckle_mob() + manifested = TRUE + Manifest() + to_chat(H, "You feel the floor closing in on your feet!") + H.Weaken(30) + H.emote("scream") + H.adjustBruteLoss(10) + if(!eating) + addtimer(CALLBACK(src, /mob/living/simple_animal/hostile/floor_cluwne/.proc/Grab, H), 70) + for(var/turf/simulated/floor/O in range(src, 6)) + O.MakeSlippery(TURF_WET_LUBE, 20) + playsound(src, 'sound/effects/meteorimpact.ogg', 30, 1) + + eating = TRUE + + +/mob/living/simple_animal/hostile/floor_cluwne/proc/Grab(mob/living/carbon/human/H) + to_chat(H, "You feel a cold, gloved hand clamp down on your ankle!") + for(var/I in 1 to get_dist(src, H)) + + if(do_after(src, 10, target = H)) + step_towards(H, src) + playsound(H, pick('sound/effects/bodyscrape-01.ogg', 'sound/effects/bodyscrape-02.ogg'), 20, 1, -4) + H.emote("scream") + if(prob(25)) + playsound(src, pick('sound/hallucinations/growl1.ogg', 'sound/hallucinations/growl2.ogg', 'sound/items/bikehorn.ogg'), 50, 1) + + if(get_dist(src,H) <= 1) + visible_message("[src] begins dragging [H] under the floor!") + + if(do_after(src, 50, target = H) && eating) + H.BecomeBlind() + H.layer = GAME_PLANE + H.invisibility = INVISIBILITY_MAXIMUM + H.mouse_opacity = 0 + H.density = FALSE + H.anchored = TRUE + addtimer(CALLBACK(src, /mob/living/simple_animal/hostile/floor_cluwne/.proc/Kill, H), 100) + H.visible_message("[src] pulls [H] under the floor!") + else//some fuck pulled away our food + stage = STAGE_TORMENT + eating = FALSE + + else//STOP PULLING AWAY THE FOOD + eating = FALSE + stage = STAGE_TORMENT + + manifested = FALSE + Manifest() + + +/mob/living/simple_animal/hostile/floor_cluwne/proc/Kill(mob/living/carbon/human/H) + playsound(H, 'sound/spookoween/scary_horn2.ogg', 100, 0) + var/old_color = H.client.color + var/red_splash = list(1,0,0,0.8,0.2,0, 0.8,0,0.2,0.1,0,0) + var/pure_red = list(0,0,0,0,0,0,0,0,0,1,0,0) + H.client.color = pure_red + + animate(H.client, color = red_splash, time = 10, easing = SINE_EASING|EASE_OUT) + for(var/turf/T in orange(H, 4)) + H.add_splatter_floor(T) + if(do_after(src, 50, target = H)) + + + if(prob(50) || smiting) + H.makeCluwne() + + H.adjustBruteLoss(30) + H.adjustBrainLoss(100) + for(var/I in H.bodyparts) + var/obj/item/organ/external/O = I + if(O.name == "head")//irksome runtimes + O.droplimb() + continue + O.drop_organs() + O.droplimb() + + Reset_View(FALSE, old_color, H) + H.CureBlind() + H.layer = initial(H.layer) + H.invisibility = initial(H.invisibility) + H.mouse_opacity = initial(H.mouse_opacity) + H.density = initial(H.density) + H.anchored = initial(H.anchored) + + eating = FALSE + if(prob(2)) + switch_stage = switch_stage * 0.75 //he gets a chance to be faster after each feast + if(smiting) + playsound(loc, 'sound/spookoween/scary_horn2.ogg', 100, 0, -4) + qdel(src) + else + Acquire_Victim() + + interest = 0 + +//manifestation animation +/obj/effect/temp_visual/fcluwne_manifest + icon = 'icons/turf/floors.dmi' + icon_state = "fcluwne_manifest" + layer = TURF_LAYER + duration = 150 + randomdir = FALSE + + +/obj/effect/temp_visual/fcluwne_manifest/New() + . = ..() + playsound(src, 'sound/spookoween/scary_clown_appear.ogg', 100, 1) + +#undef STAGE_HAUNT +#undef STAGE_SPOOK +#undef STAGE_TORMENT +#undef STAGE_ATTACK +#undef MANIFEST_DELAY \ No newline at end of file diff --git a/icons/turf/floors.dmi b/icons/turf/floors.dmi index 2f493a8acc1..135266854fc 100644 Binary files a/icons/turf/floors.dmi and b/icons/turf/floors.dmi differ diff --git a/paradise.dme b/paradise.dme index 961c6e12ec7..276626669c5 100644 --- a/paradise.dme +++ b/paradise.dme @@ -1137,6 +1137,7 @@ #include "code\modules\admin\verbs\randomverbs.dm" #include "code\modules\admin\verbs\serialization.dm" #include "code\modules\admin\verbs\space_transitions.dm" +#include "code\modules\admin\verbs\spawnfloorcluwne.dm" #include "code\modules\admin\verbs\striketeam.dm" #include "code\modules\admin\verbs\striketeam_syndicate.dm" #include "code\modules\admin\verbs\ticklag.dm" @@ -1377,6 +1378,7 @@ #include "code\modules\events\event_container.dm" #include "code\modules\events\event_manager.dm" #include "code\modules\events\false_alarm.dm" +#include "code\modules\events\floorcluwne.dm" #include "code\modules\events\grid_check.dm" #include "code\modules\events\immovable_rod.dm" #include "code\modules\events\infestation.dm" @@ -1841,6 +1843,7 @@ #include "code\modules\mob\living\simple_animal\hostile\deathsquid.dm" #include "code\modules\mob\living\simple_animal\hostile\faithless.dm" #include "code\modules\mob\living\simple_animal\hostile\feral_cat.dm" +#include "code\modules\mob\living\simple_animal\hostile\floorcluwne.dm" #include "code\modules\mob\living\simple_animal\hostile\giant_spider.dm" #include "code\modules\mob\living\simple_animal\hostile\headcrab.dm" #include "code\modules\mob\living\simple_animal\hostile\hivebot.dm" diff --git a/sound/effects/bodyscrape-01.ogg b/sound/effects/bodyscrape-01.ogg new file mode 100644 index 00000000000..48c7563412b Binary files /dev/null and b/sound/effects/bodyscrape-01.ogg differ diff --git a/sound/effects/bodyscrape-02.ogg b/sound/effects/bodyscrape-02.ogg new file mode 100644 index 00000000000..b870b5d7b02 Binary files /dev/null and b/sound/effects/bodyscrape-02.ogg differ diff --git a/sound/spookoween/ghost_whisper.ogg b/sound/spookoween/ghost_whisper.ogg new file mode 100644 index 00000000000..660b04c786d Binary files /dev/null and b/sound/spookoween/ghost_whisper.ogg differ diff --git a/sound/spookoween/insane_low_laugh.ogg b/sound/spookoween/insane_low_laugh.ogg new file mode 100644 index 00000000000..6f1ba277783 Binary files /dev/null and b/sound/spookoween/insane_low_laugh.ogg differ diff --git a/sound/spookoween/scary_clown_appear.ogg b/sound/spookoween/scary_clown_appear.ogg new file mode 100644 index 00000000000..05ab18f70b0 Binary files /dev/null and b/sound/spookoween/scary_clown_appear.ogg differ diff --git a/sound/spookoween/scary_horn.ogg b/sound/spookoween/scary_horn.ogg new file mode 100644 index 00000000000..a0b05821bb2 Binary files /dev/null and b/sound/spookoween/scary_horn.ogg differ diff --git a/sound/spookoween/scary_horn3.ogg b/sound/spookoween/scary_horn3.ogg new file mode 100644 index 00000000000..0aaefe1583a Binary files /dev/null and b/sound/spookoween/scary_horn3.ogg differ