Adds Clown Car, Ports TG Vehicle Code (#29529)

* WIP - Clown Car, Bugged key insert, buckle, driver can't drive

* WIP - Needs Movement, Testing

* Compiles

* Sealed port

* Car

* Clown Car Cannon

* Formats for comments

* Linters

* Linters again

* Linters

* Attack chains

* Dump mobs on supermatter dust, instead of dusting everything

* Supermatter bump change for clown car

* Headlights fix

* Fixes runechat for car, fixes opening trunk do-after, removed excess code

* Another do-after fix

* Fixed squishing prone people, fixed buckle bug

* Better trait handling, fixes some silicon issues

* Fixes emergency lube foam

* Fixes AI getting nullspaced on eject

* Fixes hands remaining bound when ejected after using cannon mode, fixes cannon mode, adds var-editable thing

* Updated clown car actions

* Update code/modules/vehicle/tg_vehicles/cars/clowncar.dm

Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com>

---------

Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
This commit is contained in:
PollardTheDragon
2025-07-29 23:05:06 +00:00
committed by GitHub
co-authored by Burzah
parent 6dff434902
commit 06f19cdecb
39 changed files with 778 additions and 12 deletions
+4
View File
@@ -41,3 +41,7 @@
/// from /datum/component/shelver/shelf_items()
#define COMSIG_SHELF_ATTEMPT_PICKUP "shelf_attempt_pickup"
#define SHELF_PICKUP_FAILURE (1 << 0)
/// from /datum/component/supermatter_crystal/proc/consume()
/// called on the thing consumed, passes the thing which consumed it
#define COMSIG_SUPERMATTER_CONSUMED "sm_consumed_this"
+2
View File
@@ -68,6 +68,8 @@
#define ismecha(A) (istype(A, /obj/mecha))
#define isclowncar(A) (istype(A, /obj/tgvehicle/sealed/car/clowncar))
#define iseffect(A) (istype(A, /obj/effect))
#define isclothing(A) (istype(A, /obj/item/clothing))
+8
View File
@@ -43,6 +43,14 @@
#define TIMER_COOLDOWN_END(cd_source, cd_index) LAZYREMOVE(cd_source.cooldowns, cd_index)
/// Checks if a timer based cooldown is NOT finished.
#define TIMER_COOLDOWN_RUNNING(cd_source, cd_index) LAZYACCESS(cd_source.cooldowns, cd_index)
/// Checks if a timer based cooldown is finished.
#define TIMER_COOLDOWN_FINISHED(cd_source, cd_index) (!TIMER_COOLDOWN_RUNNING(cd_source, cd_index))
/*
* Stoppable timer cooldowns.
* Use indexes the same as the regular tiemr cooldowns.
+8
View File
@@ -27,6 +27,14 @@
/// Do we need a carbon, a silicon, or a small mob, to ride the vehicle?
#define RIDER_CARBON_OR_SILICON_NO_LARGE_MOBS (1<<4)
///Will this car kidnap people by ramming into them?
#define CAN_KIDNAP (1<<0)
#define CLOWN_CANNON_INACTIVE 0
#define CLOWN_CANNON_BUSY 1
#define CLOWN_CANNON_READY 2
/// The vehicle being ridden requires pixel offsets for all directions
#define RIDING_OFFSET_ALL "ALL"
+2
View File
@@ -455,6 +455,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define DOGGO_SPACESUIT "doggo_spacesuit"
#define FLOORCLUWNE "floorcluwne"
#define LOCKDOWN_TRAIT "lockdown"
#define VEHICLE_TRAIT "vehicle"
#define STAT_TRAIT "stat_trait"
#define TRANSFORMING_TRAIT "transforming"
#define BUCKLING_TRAIT "buckled"
@@ -466,6 +467,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define HOLO_CIGAR "holo_cigar"
#define GLADIATOR "gladiator"
#define PULSEDEMON_TRAIT "pulse_demon"
#define TRAIT_CLOWN_CAR_SQUISHED "squished_by_car"
/// Mentor observing
#define MENTOR_OBSERVING "mobserving"
#define TIPPED_OVER "tipped_over"
+4
View File
@@ -108,6 +108,10 @@
var/obj/mecha/M = loc
return M.click_action(A, src, params)
if(isclowncar(loc) && !modifiers["shift"])
var/obj/tgvehicle/sealed/car/clowncar/cc = loc
return cc.fire_cannon_at(A, src, params)
if(restrained())
RestrainedClickOn(A)
return
+3
View File
@@ -49,6 +49,9 @@
if(incapacitated())
return
if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED))
return
if(next_move >= world.time)
return
@@ -198,4 +198,8 @@
return
override_allow_spacemove = FALSE
/datum/component/riding/vehicle/clowncar
vehicle_move_delay = 0.6
ride_check_flags = RIDER_NEEDS_LEGS | RIDER_NEEDS_ARMS
#undef EPSILON
@@ -49,6 +49,21 @@
cost = 5
job = list("Clown")
/datum/uplink_item/jobspecific/clown_car
name = "Clown Car"
desc = "The Clown Car is the ultimate transportation method for any worthy clown! \
Simply insert your bikehorn and get in, and get ready to have the funniest ride of your life! \
You can ram any crew you come across and stuff them into your car, kidnapping them and locking them inside until \
someone saves them or they manage to crawl out. Be sure not to ram into any walls or vending machines, as the springloaded seats \
are very sensitive. Now with our included lube defense mechanism which will protect you against any angry shitcurity! \
Premium features can be unlocked with a cryptographic sequencer!"
reference = "CCR"
item = /obj/tgvehicle/sealed/car/clowncar
cost = 50
job = list("Clown")
surplus = 0
hijack_only = TRUE
//mime
/datum/uplink_item/jobspecific/caneshotgun
name = "Cane Shotgun and Assassination Shells"
@@ -2032,6 +2032,8 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
/mob/living/carbon/human/update_runechat_msg_location()
if(ismecha(loc))
runechat_msg_location = loc.UID()
else if(istgvehicle(loc))
runechat_msg_location = loc.UID()
else
return ..()
@@ -376,6 +376,12 @@ GLOBAL_LIST_INIT(robot_verbs_default, list(
module_actions.Cut()
return ..()
/mob/living/silicon/robot/update_runechat_msg_location()
if(istgvehicle(loc))
runechat_msg_location = loc.UID()
else
return ..()
/mob/living/silicon/robot/proc/pick_module()
if(module)
return
@@ -857,6 +857,7 @@
Bumped(A)
/obj/machinery/atmospherics/supermatter_crystal/proc/Consume(atom/movable/AM)
SEND_SIGNAL(AM, COMSIG_SUPERMATTER_CONSUMED, src)
if(isliving(AM))
var/mob/living/user = AM
if(user.status_flags & GODMODE)
@@ -0,0 +1,114 @@
/obj/tgvehicle/sealed/car
layer = ABOVE_MOB_LAYER
move_resist = MOVE_FORCE_VERY_STRONG
///Bitflags for special behavior such as kidnapping
var/car_traits = NONE
///Sound file(s) to play when we drive around
var/engine_sound = 'sound/vehicles/carrev.ogg'
///Set this to the length of the engine sound.
var/engine_sound_length = 2 SECONDS
///Time it takes to break out of the car.
var/escape_time = 6 SECONDS
/// How long it takes to move, cars don't use the riding component similar to mechs so we handle it ourselves
var/vehicle_move_delay = 1
/// What sound to play if someone was forced in.
var/forced_enter_sound
/// How long it takes to rev (vrrm vrrm!)
COOLDOWN_DECLARE(enginesound_cooldown)
/obj/tgvehicle/sealed/car/generate_actions()
. = ..()
if(!isnull(key_type))
initialize_controller_action_type(/datum/action/vehicle/sealed/remove_key, VEHICLE_CONTROL_DRIVE)
if(car_traits & CAN_KIDNAP)
initialize_controller_action_type(/datum/action/vehicle/sealed/dump_kidnapped_mobs, VEHICLE_CONTROL_DRIVE)
/obj/tgvehicle/sealed/car/MouseDrop_T(mob/living/M, mob/living/user)
if(!istype(M))
return FALSE
if(HAS_TRAIT(M, TRAIT_HANDS_BLOCKED) && !is_driver(M))
return FALSE
if((car_traits & CAN_KIDNAP) && isliving(user) && M != user)
var/mob/living/kidnapped = M
kidnapped.visible_message("<span class='warning'>[user] starts forcing [kidnapped] into [src]!</span>")
mob_try_forced_enter(user, kidnapped)
return ..()
/obj/tgvehicle/sealed/car/mob_try_exit(mob/future_pedestrian, mob/user, silent = FALSE)
if(future_pedestrian != user || !(LAZYACCESS(occupants, future_pedestrian) & VEHICLE_CONTROL_KIDNAPPED))
mob_exit(future_pedestrian, silent)
return TRUE
if(escape_time > 0)
to_chat(user, "<span class='notice'>You push against the back of \the [src]'s trunk to try and get out.</span>")
if(!do_after(user, escape_time, target = src))
return FALSE
to_chat(user, "<span class='danger'>[user] gets out of [src].</span>")
mob_exit(future_pedestrian, silent)
return TRUE
/obj/tgvehicle/sealed/car/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(!(car_traits & CAN_KIDNAP))
return
to_chat(user, "<span class='notice'>You start opening [src]'s trunk.</span>")
if(!do_after(user, 30, target = src))
return
if(return_amount_of_controllers_with_flag(VEHICLE_CONTROL_KIDNAPPED))
to_chat(user, "<span class='notice'>The people stuck in [src]'s trunk all come tumbling out.</span>?")
dump_specific_mobs(VEHICLE_CONTROL_KIDNAPPED)
return
to_chat(user, "<span class='notice'>It seems [src]'s trunk was empty.</span>")
/// Attempts to force a mob into the car
/obj/tgvehicle/sealed/car/proc/mob_try_forced_enter(mob/forcer, mob/kidnapped, silent = FALSE)
if(occupant_amount() >= max_occupants)
return FALSE
var/atom/old_loc = loc
var/enter_delay = get_enter_delay(kidnapped)
if(enter_delay == 0 || do_after(forcer, enter_delay, target = kidnapped, extra_checks=CALLBACK(src, TYPE_PROC_REF(/obj/tgvehicle/sealed/car, is_car_stationary), old_loc)))
mob_forced_enter(kidnapped, silent)
return TRUE
return FALSE
/// Callback proc to check for
/obj/tgvehicle/sealed/car/proc/is_car_stationary(atom/old_loc)
return (old_loc == loc)
/// Proc called when someone is forcefully stuffedd into a car
/obj/tgvehicle/sealed/car/proc/mob_forced_enter(mob/kidnapped, silent = FALSE)
if(!silent)
kidnapped.visible_message("<span class='warning'>[kidnapped] is forced into \the [src]!</span>")
if(forced_enter_sound)
playsound(src, forced_enter_sound, 70, TRUE)
kidnapped.forceMove(src)
add_occupant(kidnapped, VEHICLE_CONTROL_KIDNAPPED, TRUE)
/obj/tgvehicle/sealed/car/Destroy()
explosion(src, heavy_impact_range = 1, light_impact_range = 2, flash_range = 3, adminlog = FALSE)
log_attack("[src] exploded due to destruction")
return ..()
/obj/tgvehicle/sealed/car/relaymove(mob/living/user, direction)
if(is_driver(user) && canmove && (!key_type || istype(inserted_key, key_type)))
vehicle_move(direction)
return TRUE
/obj/tgvehicle/sealed/car/vehicle_move(direction)
if(!COOLDOWN_FINISHED(src, cooldown_vehicle_move))
return FALSE
COOLDOWN_START(src, cooldown_vehicle_move, vehicle_move_delay)
if(COOLDOWN_FINISHED(src, enginesound_cooldown))
COOLDOWN_START(src, enginesound_cooldown, engine_sound_length)
playsound(get_turf(src), engine_sound, 100, TRUE)
..()
if(trailer)
var/dir_to_move = get_dir(trailer.loc, loc)
var/did_move = step(trailer, direction)
if(did_move)
step(trailer, dir_to_move)
return did_move
after_move(direction)
return step(src, direction)
@@ -0,0 +1,297 @@
/obj/tgvehicle/sealed/car/clowncar
name = "clown car"
desc = "How someone could even fit in there is beyond me."
icon_state = "clowncar"
max_integrity = 150
max_occupants = 50
key_type = /obj/item/bikehorn
enter_sound = 'sound/effects/clowncar/door_close.ogg'
exit_sound = 'sound/effects/clowncar/door_open.ogg'
car_traits = CAN_KIDNAP
armor = list(melee = 70, bullet = 40, laser = 40, energy = 40, bomb = 30, rad = 0, fire = 80, acid = 80)
enter_delay = 4 SECONDS
access_provider_flags = VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_KIDNAPPED
/// list of headlight colors we use to pick through when we have party mode due to emag
var/headlight_colors = list(COLOR_RED, COLOR_ORANGE, COLOR_YELLOW, COLOR_LIME, COLOR_BLUE_LIGHT, COLOR_CYAN, COLOR_PURPLE)
/// Cooldown time inbetween [/obj/tgvehicle/sealed/car/clowncar/proc/roll_the_dice()] usages
var/dice_cooldown_time = 15 SECONDS
/// Current status of the cannon, alternates between CLOWN_CANNON_INACTIVE, CLOWN_CANNON_BUSY and CLOWN_CANNON_READY
var/cannonmode = CLOWN_CANNON_INACTIVE
/// Does the driver require the clown role to drive it
var/enforce_clown_role = TRUE
/// Emag Button Cooldown
var/last_emag_button_use = 0
/// How fast can you fire the cannon
var/cannon_fire_delay = 0.5 SECONDS
COOLDOWN_DECLARE(cannon_cooldown)
/obj/tgvehicle/sealed/car/clowncar/Initialize(mapload)
. = ..()
START_PROCESSING(SSobj,src)
/obj/tgvehicle/sealed/car/clowncar/process()
if(headlights_toggle && emagged)
set_light(headlight_range, headlight_power, pick(headlight_colors))
/obj/tgvehicle/sealed/car/clowncar/generate_actions()
. = ..()
initialize_controller_action_type(/datum/action/vehicle/sealed/horn, VEHICLE_CONTROL_DRIVE)
initialize_controller_action_type(/datum/action/vehicle/sealed/headlights, VEHICLE_CONTROL_DRIVE)
initialize_controller_action_type(/datum/action/vehicle/sealed/thank, VEHICLE_CONTROL_KIDNAPPED)
/obj/tgvehicle/sealed/car/clowncar/auto_assign_occupant_flags(mob/M)
if(ishuman(M))
var/mob/living/carbon/human/H = M
// Only clowns can drive the car, unless an admin varedits the car to allow someone else to drive.
if(H.mind?.assigned_role == "Clown" || (!enforce_clown_role && length(return_controllers_with_flag(VEHICLE_CONTROL_DRIVE)) < 1))
add_control_flags(H, VEHICLE_CONTROL_DRIVE)
playsound(src, 'sound/effects/clowncar/door_close.ogg', 70, TRUE)
log_game("[M] has entered [src] as a possible driver")
return
add_control_flags(M, VEHICLE_CONTROL_KIDNAPPED)
/obj/tgvehicle/sealed/car/clowncar/mob_forced_enter(mob/M)
. = ..()
playsound(src, pick('sound/effects/clowncar/clowncar_load1.ogg', 'sound/effects/clowncar/clowncar_load2.ogg'), 75)
if(iscarbon(M))
var/mob/living/carbon/forced_mob = M
if(forced_mob.reagents.has_reagent("dublindrop"))
var/reagent_amount = forced_mob.reagents.get_reagent_amount("dublindrop")
forced_mob.reagents.del_reagent("dublindrop")
if(reagent_amount >= 30)
message_admins("[ADMIN_LOOKUPFLW(forced_mob)] was forced into a clown car with [reagent_amount] unit(s) of Irish Car Bomb, causing an explosion.")
audible_message("<span class='warning'>You hear a rattling sound coming from the engine. That can't be good...</span>", null, 1)
addtimer(CALLBACK(src, PROC_REF(irish_car_bomb)), 5 SECONDS)
// We don't want everything in the clown car to get dusted when it touches the supermatter, so dump the mobs.
/obj/tgvehicle/sealed/car/clowncar/on_entered_supermatter(atom/movable/vehicle, atom/movable/supermatter)
dump_mobs(TRUE)
/obj/tgvehicle/sealed/car/clowncar/proc/irish_car_bomb()
dump_mobs()
explosion(src, light_impact_range = 1)
/obj/tgvehicle/sealed/car/clowncar/attack_animal(mob/living/simple_animal/user, list/modifiers)
if((user.loc != src) || user.environment_smash >= ENVIRONMENT_SMASH_WALLS)
return ..()
/obj/tgvehicle/sealed/car/clowncar/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
. = ..()
if(prob(33))
visible_message("<span class='warning'>[src] spews out a ton of space lube!</span>")
var/datum/effect_system/foam_spread/L = new()
var/datum/reagents/foamreagent = new /datum/reagents(200)
foamreagent.add_reagent("lube", 200)
L.set_up(40, get_turf(src), foamreagent)
L.start()
/obj/tgvehicle/sealed/car/clowncar/attacked_by(obj/item/I, mob/living/user)
. = ..()
if(!istype(I, /obj/item/food/grown/banana))
return
var/obj/item/food/grown/banana/banana = I
obj_integrity += min(banana.seed.potency, max_integrity-obj_integrity)
to_chat(user, "<span class='warning'>You use the [banana] to repair [src]!</span>")
qdel(banana)
/obj/tgvehicle/sealed/car/clowncar/Bump(atom/bumped)
. = ..()
if(isliving(bumped))
if(ismegafauna(bumped)) // No, you cannot kidnap megafauna with the clown car
return
var/mob/living/hittarget_living = bumped
if(istype(hittarget_living, /mob/living/basic/deer))
visible_message("<span class='warning'>[src] careens into [hittarget_living]! Oh the humanity!</span>")
for(var/mob/living/carbon/carbon_occupant in occupants)
if(prob(35)) // Note: The randomstep on dump_mobs throws occupants into each other and often causes wounds regardless.
continue
hittarget_living.adjustBruteLoss(200)
new /obj/effect/decal/cleanable/blood/splatter(get_turf(hittarget_living))
dump_mobs(TRUE)
playsound(src, 'sound/effects/clowncar/car_crash.ogg', 100)
return
if(iscarbon(hittarget_living))
var/mob/living/carbon/C = hittarget_living
C.Stun(4 SECONDS) // I play to make sprites go horizontal
hittarget_living.visible_message("<span class='warning'>[src] rams into [hittarget_living] and sucks [hittarget_living.p_them()] up!</span>")
mob_forced_enter(hittarget_living)
playsound(src, pick('sound/effects/clowncar/clowncar_ram1.ogg', 'sound/effects/clowncar/clowncar_ram2.ogg', 'sound/effects/clowncar/clowncar_ram3.ogg'), 75)
log_attack(src, hittarget_living, "sucked up")
return
if(isturf(bumped))
var/turf/bumped_turf = bumped
if(!bumped_turf.is_blocked_turf())
return
if(istype(bumped, /obj/machinery/door))
var/obj/machinery/door/conditionalwall = bumped
for(var/mob/occupant as anything in return_controllers_with_flag(access_provider_flags))
if(conditionalwall.allowed(occupant))
return
if(conditionalwall.operating)
return
visible_message("<span class='warning'>[src] rams into [bumped] and crashes!</span>")
playsound(src, pick('sound/effects/clowncar/clowncar_crash1.ogg', 'sound/effects/clowncar/clowncar_crash2.ogg'), 75)
playsound(src, 'sound/effects/clowncar/clowncar_crashpins.ogg', 75)
dump_mobs(TRUE)
log_attack(src, bumped, "crashed into", null)
/obj/tgvehicle/sealed/car/clowncar/after_move(direction)
..()
if(!has_gravity(src))
return
var/mob/possible_pancake = locate(/mob/living/carbon) in loc
if(!possible_pancake)
return
if(possible_pancake.loc == src)
return
if(!iscarbon(possible_pancake))
return
var/mob/living/carbon/target_pancake = possible_pancake
if(target_pancake.body_position != LYING_DOWN)
return
if(HAS_TRAIT(target_pancake, TRAIT_CLOWN_CAR_SQUISHED))
return
target_pancake.visible_message("<span class='warning'>[src] runs over [target_pancake], flattening [target_pancake.p_them()] like a pancake!</span>")
handle_squish_carbon(target_pancake, 30)
ADD_TRAIT(target_pancake, TRAIT_CLOWN_CAR_SQUISHED, "clown_car")
addtimer(CALLBACK(src, PROC_REF(allow_resquish), target_pancake), 5 SECONDS)
target_pancake.Stun(2 SECONDS)
playsound(target_pancake, 'sound/effects/clowncar/cartoon_splat.ogg', 75)
log_attack(src, target_pancake, "ran over")
/obj/tgvehicle/sealed/car/clowncar/emag_act(mob/user, obj/item/card/emag/emag_card)
if(emagged)
return FALSE
emagged = TRUE
to_chat(user, "<span class='warning'>You scramble [src]'s child safety lock, and a panel with six colorful buttons appears!</span>")
initialize_controller_action_type(/datum/action/vehicle/sealed/roll_the_dice, VEHICLE_CONTROL_DRIVE)
initialize_controller_action_type(/datum/action/vehicle/sealed/cannon, VEHICLE_CONTROL_DRIVE)
return TRUE
/obj/tgvehicle/sealed/car/clowncar/obj_destruction(damage_flag)
playsound(src, 'sound/misc/sadtrombone.ogg', 100)
STOP_PROCESSING(SSobj,src)
return ..()
/obj/tgvehicle/sealed/car/clowncar/proc/allow_resquish(mob/living/carbon/pancake)
REMOVE_TRAIT(pancake, TRAIT_CLOWN_CAR_SQUISHED, "clown_car")
/**
* Plays a random funky effect
* Only available while car is emagged
* Possible effects:
* * Spawn bananapeel
* * Spawn random reagent foam
* * Make the clown car look like a singulo temporarily
* * Spawn Laughing chem gas
* * Drop oil
* * Fart and make everyone nearby laugh
*/
/obj/tgvehicle/sealed/car/clowncar/proc/roll_the_dice(mob/user)
if(last_emag_button_use + dice_cooldown_time > world.time)
to_chat(user, "<span class=notice'>The button panel is currently recharging.</span>")
return
last_emag_button_use = world.time
switch(rand(1,6))
if(1)
visible_message("<span class='warning'>[user] presses one of the colorful buttons on [src], and a special banana peel drops out of it.</span>")
new /obj/item/grown/bananapeel/specialpeel(loc)
if(2)
visible_message("<span class='warning'>[user] presses one of the colorful buttons on [src], and unknown chemicals flood out of it.</span>")
var/datum/reagents/randomchems = new/datum/reagents(300)
randomchems.my_atom = src
randomchems.add_reagent(get_random_reagent_id(), 100)
var/datum/effect_system/foam_spread/foam = new()
foam.set_up(200, src, randomchems)
foam.start()
if(3)
visible_message("<span class='warning'>[user] presses one of the colorful buttons on [src], and the clown car turns on its singularity disguise system.</span>")
icon = 'icons/obj/singularity.dmi'
icon_state = "singularity_s1"
addtimer(CALLBACK(src, PROC_REF(reset_icon)), 10 SECONDS)
if(4)
visible_message("<span class='warning'>[user] presses one of the colorful buttons on [src], and the clown car spews out a cloud of confetti all over the place.</span>")
confettisize(src, 50, 8)
if(5)
visible_message("<span class='warning'>[user] presses one of the colorful buttons on [src], and the clown car starts dropping a lubricant trail.</span>")
RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(cover_in_oil))
addtimer(CALLBACK(src, PROC_REF(stop_dropping_oil)), 3 SECONDS)
if(6)
visible_message("<span class='warning'>[user] presses one of the colorful buttons on [src], and the clown car lets out a comedic toot.</span>")
playsound(src, 'sound/effects/clowncar/clowncar_fart.ogg', 100)
for(var/mob/living/L in orange(loc, 6))
L.emote("laugh")
for(var/mob/living/L as anything in occupants)
L.emote("laugh")
/// resets the icon and iconstate of the clowncar after it was set to singulo states
/obj/tgvehicle/sealed/car/clowncar/proc/reset_icon()
icon = initial(icon)
icon_state = initial(icon_state)
/// Deploys oil when the clowncar moves in oil deploy mode
/obj/tgvehicle/sealed/car/clowncar/proc/cover_in_oil()
SIGNAL_HANDLER
var/turf/simulated/T = get_turf(src)
T.MakeSlippery(TURF_WET_LUBE)
/// Stops dropping oil after the time has run up
/obj/tgvehicle/sealed/car/clowncar/proc/stop_dropping_oil()
UnregisterSignal(src, COMSIG_MOVABLE_MOVED)
/// Toggles the on and off state of the clown cannon that shoots random kidnapped people
/obj/tgvehicle/sealed/car/clowncar/proc/toggle_cannon(mob/user)
if(cannonmode == CLOWN_CANNON_BUSY)
to_chat(user, "<span class='notice'>Please wait for the vehicle to finish its current action first.</span>")
return
if(cannonmode) // cannon active, deactivate
flick("clowncar_fromfire", src)
icon_state = "clowncar"
addtimer(CALLBACK(src, PROC_REF(deactivate_cannon)), 2 SECONDS)
playsound(src, 'sound/effects/clowncar/clowncar_cannonmode2.ogg', 75)
visible_message("<span class='warning'>[src] starts going back into mobile mode.</span>")
else
canmove = FALSE // anchor and activate cannon
flick("clowncar_tofire", src)
icon_state = "clowncar_fire"
visible_message("<span class='warning'>[src] opens up and reveals a large cannon.</span>")
addtimer(CALLBACK(src, PROC_REF(activate_cannon)), 2 SECONDS)
playsound(src, 'sound/effects/clowncar/clowncar_cannonmode1.ogg', 75)
cannonmode = CLOWN_CANNON_BUSY
/// Finalizes cannon activation
/obj/tgvehicle/sealed/car/clowncar/proc/activate_cannon()
var/mouse_pointer = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
cannonmode = CLOWN_CANNON_READY
for(var/mob/living/driver as anything in return_controllers_with_flag(VEHICLE_CONTROL_DRIVE))
REMOVE_TRAIT(driver, TRAIT_HANDS_BLOCKED, VEHICLE_TRAIT)
if(driver.client.mouse_pointer_icon == initial(driver.client.mouse_pointer_icon))
driver.client.mouse_pointer_icon = mouse_pointer
/// Finalizes cannon deactivation
/obj/tgvehicle/sealed/car/clowncar/proc/deactivate_cannon()
canmove = TRUE
cannonmode = CLOWN_CANNON_INACTIVE
for(var/mob/living/driver as anything in return_controllers_with_flag(VEHICLE_CONTROL_DRIVE))
driver.client.mouse_pointer_icon = initial(driver.client.mouse_pointer_icon)
ADD_TRAIT(driver, TRAIT_HANDS_BLOCKED, VEHICLE_TRAIT)
/// Fires the cannon where the user clicks
/obj/tgvehicle/sealed/car/clowncar/proc/fire_cannon_at(atom/target, mob/user, list/modifiers)
if(cannonmode != CLOWN_CANNON_READY || !length(return_controllers_with_flag(VEHICLE_CONTROL_KIDNAPPED)))
return
if(!COOLDOWN_FINISHED(src, cannon_cooldown))
return
var/mob/living/unlucky_sod = pick(return_controllers_with_flag(VEHICLE_CONTROL_KIDNAPPED))
mob_exit(unlucky_sod, silent = TRUE)
flick("clowncar_recoil", src)
playsound(src, pick('sound/effects/clowncar/carcannon1.ogg', 'sound/effects/clowncar/carcannon2.ogg', 'sound/effects/clowncar/carcannon3.ogg'), 75)
unlucky_sod.throw_at(target, 10, 2)
COOLDOWN_START(src, cannon_cooldown, cannon_fire_delay)
log_attack(user, unlucky_sod, "fired towards [target]") // this doesn't catch if the mob hits something between the car and the target
return COMSIG_MOB_CANCEL_CLICKON
+3 -3
View File
@@ -300,16 +300,16 @@
/obj/tgvehicle/scooter/skateboard/wrench_act(mob/living/user, obj/item/I)
return
/obj/tgvehicle/scooter/skateboard/improvised/attackby__legacy__attackchain(obj/item/I, mob/user, params)
/obj/tgvehicle/scooter/skateboard/improvised/item_interaction(mob/living/user, obj/item/I, list/modifiers)
if(!istype(I, /obj/item/stack/rods))
return ..()
var/obj/item/stack/S = I
if(S.get_amount() < 2)
return
return ITEM_INTERACT_COMPLETE
to_chat(user, "<span class='notice'>You begin making handlebars for [src].</span>")
if(do_after(user, 2.5 SECONDS, target = src))
if(!loc || !S || S.get_amount() < 2 || !S.use(2))
return
return ITEM_INTERACT_COMPLETE
to_chat(user, "<span class='notice'>You add the rods to [src], creating handlebars.</span>")
var/obj/tgvehicle/scooter/skaterskoot = new(loc)
if(has_buckled_mobs())
+175
View File
@@ -0,0 +1,175 @@
/obj/tgvehicle/sealed
can_buckle = FALSE
/// How long it takes to enter the vehice
var/enter_delay = 2 SECONDS
/// A special curser you may get while driving
var/mouse_pointer
/// Are the headlights on?
var/headlights_toggle = FALSE
/// How far do the headlights reach?
var/headlight_range = 6
/// How powerful are the headlights?
var/headlight_power = 2
/// Determines which occupants provide access when bumping into doors
var/access_provider_flags = VEHICLE_CONTROL_DRIVE
/obj/tgvehicle/sealed/Initialize(mapload)
. = ..()
RegisterSignal(src, COMSIG_SUPERMATTER_CONSUMED, PROC_REF(on_entered_supermatter))
/obj/tgvehicle/sealed/generate_actions()
. = ..()
initialize_passenger_action_type(/datum/action/vehicle/sealed/climb_out)
/obj/tgvehicle/sealed/generate_action_type()
var/datum/action/vehicle/sealed/E = ..()
. = E
if(istype(E))
E.vehicle_entered_target = src
/obj/tgvehicle/sealed/MouseDrop_T(mob/living/M, mob/living/user)
if(!istype(M))
return ..()
mob_try_enter(M)
return ..()
/obj/tgvehicle/sealed/Exited(atom/movable/gone, direction)
. = ..()
if(ismob(gone))
remove_occupant(gone)
// so that we can check the access of the vehicle's occupants. Ridden vehicles do this in the riding component, but these don't have that
/obj/tgvehicle/sealed/Bump(atom/A)
. = ..()
if(istype(A, /obj/machinery/door))
var/obj/machinery/door/conditionalwall = A
for(var/mob/occupant as anything in return_controllers_with_flag(access_provider_flags))
if(!conditionalwall.allowed(occupant))
continue
if(conditionalwall.operating || conditionalwall.emagged || conditionalwall.foam_level)
return
conditionalwall.bumpopen(occupant)
/obj/tgvehicle/sealed/after_add_occupant(mob/M)
. = ..()
ADD_TRAIT(M, TRAIT_HANDS_BLOCKED, VEHICLE_TRAIT)
/obj/tgvehicle/sealed/after_remove_occupant(mob/M)
. = ..()
REMOVE_TRAIT(M, TRAIT_HANDS_BLOCKED, VEHICLE_TRAIT)
/obj/tgvehicle/sealed/proc/mob_try_enter(mob/rider)
if(!istype(rider))
return FALSE
var/enter_delay = get_enter_delay(rider)
if(enter_delay == 0)
if(enter_checks(rider))
mob_enter(rider)
return TRUE
return FALSE
if(do_after(rider, enter_delay, target = src))
if(!enter_checks(rider))
return FALSE
mob_enter(rider)
return TRUE
return FALSE
/// returns enter do_after delay for the given mob in ticks
/obj/tgvehicle/sealed/proc/get_enter_delay(mob/M)
return enter_delay
/// Extra checks to perform during the do_after to enter the vehicle
/obj/tgvehicle/sealed/proc/enter_checks(mob/M)
return occupant_amount() < max_occupants
/obj/tgvehicle/sealed/proc/mob_enter(mob/M, silent = FALSE)
if(!istype(M))
return FALSE
if(!silent)
M.visible_message("<span class='notice'>[M] climbs into \the [src]!</span>")
M.forceMove(src)
add_occupant(M)
return TRUE
/obj/tgvehicle/sealed/proc/mob_try_exit(mob/M, mob/user, silent = FALSE, randomstep = FALSE)
mob_exit(M, silent, randomstep)
/obj/tgvehicle/sealed/proc/mob_exit(mob/M, silent = FALSE, randomstep = FALSE)
if(!istype(M))
return FALSE
remove_occupant(M)
M.forceMove(exit_location(M))
if(randomstep)
var/turf/target_turf = get_step(exit_location(M), pick(GLOB.cardinal))
M.throw_at(target_turf, 5, 10)
if(!silent)
M.visible_message("<span class='notice'>[M] drops out of \the [src]!</span>")
return TRUE
/obj/tgvehicle/sealed/proc/exit_location(M)
return drop_location()
/obj/tgvehicle/sealed/item_interaction(mob/living/user, obj/item/I, list/modifiers)
if(key_type && !is_key(inserted_key) && is_key(I))
if(I.flags & NODROP || !user.drop_item() || !I.forceMove(src))
to_chat(user, "<span class='warning'>[I] is stuck to your hand!</span>")
return ITEM_INTERACT_COMPLETE
to_chat(user, "<span class='notice'>You insert [I] into [src].</span>")
if(inserted_key) // just in case there's an invalid key
inserted_key.forceMove(drop_location())
inserted_key = I
inserted_key.forceMove(src)
return ITEM_INTERACT_COMPLETE
return ..()
/obj/tgvehicle/sealed/proc/remove_key(mob/user)
if(!inserted_key)
to_chat(user, "<span class='warning'>There is no key in [src]!</span>")
return
if(!is_occupant(user) || !(occupants[user] & VEHICLE_CONTROL_DRIVE))
to_chat(user, "<span class='warning'>You must be driving [src] to remove [src]'s key!</span>")
return
to_chat(user, "<span class='notice'>You remove [inserted_key] from [src].</span>")
user.put_in_hands(inserted_key)
inserted_key = null
/obj/tgvehicle/sealed/Destroy()
dump_mobs()
return ..()
/obj/tgvehicle/sealed/proc/dump_mobs(randomstep = TRUE)
for(var/i in occupants)
mob_exit(i, randomstep = randomstep)
if(iscarbon(i))
var/mob/living/carbon/Carbon = i
Carbon.Stun(4 SECONDS)
/obj/tgvehicle/sealed/proc/dump_specific_mobs(flag, randomstep = TRUE)
for(var/i in occupants)
if(!(occupants[i] & flag))
continue
mob_exit(i, randomstep = randomstep)
if(iscarbon(i))
var/mob/living/carbon/C = i
C.Stun(4 SECONDS)
/obj/tgvehicle/sealed/AllowDrop()
return FALSE
/obj/tgvehicle/sealed/relaymove(mob/living/user, direction)
if(canmove)
vehicle_move(direction)
return TRUE
/// Sinced sealed vehicles (cars and mechs) don't have riding components, the actual movement is handled here from [/obj/tgvehicle/sealed/proc/relaymove]
/obj/tgvehicle/sealed/proc/vehicle_move(direction)
return FALSE
/// When we touch a crystal, kill everything inside us
/obj/tgvehicle/sealed/proc/on_entered_supermatter(atom/movable/vehicle, atom/movable/supermatter)
SIGNAL_HANDLER // COMSIG_SUPERMATTER_CONSUMED
for(var/mob/passenger as anything in occupants)
if(!is_ai(passenger))
passenger.Bump(supermatter)
@@ -287,3 +287,116 @@
animate(board, pixel_y = -6, time = 0.3 SECONDS)
board.unbuckle_mob(rider)
addtimer(CALLBACK(board, TYPE_PROC_REF(/obj/tgvehicle/scooter/skateboard, pick_up_board), rider), 0.5 SECONDS) // so the board can still handle "picking it up"
/datum/action/vehicle/sealed
check_flags = AB_CHECK_IMMOBILE | AB_CHECK_CONSCIOUS
var/obj/tgvehicle/sealed/vehicle_entered_target
/datum/action/vehicle/sealed/Destroy()
vehicle_entered_target = null
return ..()
/datum/action/vehicle/sealed/climb_out
name = "Climb Out"
desc = "Climb out of your vehicle!"
button_icon_state = "car_eject"
/datum/action/vehicle/sealed/climb_out/Trigger(trigger_flags)
if(..() && istype(vehicle_entered_target))
vehicle_entered_target.mob_try_exit(owner, owner)
/datum/action/vehicle/sealed/remove_key
name = "Remove key"
desc = "Take your key out of the vehicle's ignition."
button_icon_state = "car_removekey"
/datum/action/vehicle/sealed/remove_key/Trigger(trigger_flags)
vehicle_entered_target.remove_key(owner)
// Clown car stuff
/datum/action/vehicle/sealed/horn
name = "Honk Horn"
desc = "Honk your classy horn."
button_icon_state = "car_horn"
var/horn_sound = 'sound/items/airhorn.ogg'
var/horn_cooldown = 60
var/last_horn = 0
/datum/action/vehicle/sealed/horn/Trigger(trigger_flags)
if(last_horn + horn_cooldown > world.time)
return
last_horn = world.time
vehicle_entered_target.visible_message("<span class='warning'>[vehicle_entered_target] loudly honks!</span>")
to_chat(owner, "<span class=notice'>You press [vehicle_entered_target]'s horn.</span>")
if(istype(vehicle_entered_target.inserted_key, /obj/item/bikehorn))
playsound(vehicle_entered_target, 'sound/items/bikehorn.ogg', 75) // The bikehorn plays a sound instead
return
playsound(vehicle_entered_target, horn_sound, 75)
/datum/action/vehicle/sealed/headlights
name = "Toggle Headlights"
desc = "Turn on your brights!"
button_icon_state = "car_headlights"
/datum/action/vehicle/sealed/headlights/Trigger(trigger_flags)
to_chat(owner, "<span class=notice'>You flip the switch for the vehicle's headlights.</span>")
vehicle_entered_target.headlights_toggle = !vehicle_entered_target.headlights_toggle
if(vehicle_entered_target.headlights_toggle)
vehicle_entered_target.set_light(vehicle_entered_target.headlight_range, vehicle_entered_target.headlight_power)
else
vehicle_entered_target.remove_light()
vehicle_entered_target.update_appearance()
playsound(owner, vehicle_entered_target.headlights_toggle ? 'sound/weapons/gun_interactions/pistol_magin.ogg' : 'sound/weapons/gun_interactions/pistol_magout.ogg', 40, TRUE)
/datum/action/vehicle/sealed/dump_kidnapped_mobs
name = "Dump Kidnapped Mobs"
desc = "Dump all objects and people in your car on the floor."
button_icon_state = "car_dump"
/datum/action/vehicle/sealed/dump_kidnapped_mobs/Trigger(trigger_flags)
vehicle_entered_target.visible_message("<span class='warning'>[vehicle_target] starts dumping the people inside of it.</span>")
vehicle_entered_target.dump_specific_mobs(VEHICLE_CONTROL_KIDNAPPED)
/datum/action/vehicle/sealed/roll_the_dice
name = "Press Colorful Button"
desc = "Press one of those colorful buttons on your display panel!"
button_icon_state = "car_rtd"
/datum/action/vehicle/sealed/roll_the_dice/Trigger(trigger_flags)
if(!istype(vehicle_entered_target, /obj/tgvehicle/sealed/car/clowncar))
return
var/obj/tgvehicle/sealed/car/clowncar/C = vehicle_entered_target
C.roll_the_dice(owner)
/datum/action/vehicle/sealed/cannon
name = "Toggle Siege Mode"
desc = "Destroy them with their own fodder!"
button_icon_state = "car_cannon"
/datum/action/vehicle/sealed/cannon/Trigger(trigger_flags)
if(!istype(vehicle_entered_target, /obj/tgvehicle/sealed/car/clowncar))
return
var/obj/tgvehicle/sealed/car/clowncar/C = vehicle_entered_target
C.toggle_cannon(owner)
/datum/action/vehicle/sealed/thank
name = "Thank the Clown Car Driver"
desc = "They're just doing their job."
button_icon_state = "car_thanktheclown"
COOLDOWN_DECLARE(thank_time_cooldown)
/datum/action/vehicle/sealed/thank/Trigger(trigger_flags)
if(!istype(vehicle_entered_target, /obj/tgvehicle/sealed/car/clowncar))
return
if(!COOLDOWN_FINISHED(src, thank_time_cooldown))
return
COOLDOWN_START(src, thank_time_cooldown, 6 SECONDS)
var/obj/tgvehicle/sealed/car/clowncar/clown_car = vehicle_entered_target
var/list/mob/drivers = clown_car.return_drivers()
if(!length(drivers))
to_chat(owner, "<span class='warning'>You prepare to thank the driver, only to realize that they don't exist.</span>")
return
var/mob/clown = pick(drivers)
owner.say("Thank you for the fun ride, [clown.name]!")
@@ -18,6 +18,8 @@
var/max_occupants = 1
////Maximum amount of drivers
var/max_drivers = 1
/// Whether the vehicle is currently able to move
var/canmove = TRUE
/**
* If the driver needs a certain item in hand (or inserted, for vehicles) to drive this. For vehicles, this must be duplicated on their riding component subtype
* [/datum/component/riding/var/keytype] variable because only a few specific checks are handled here with this var, and the majority of it is on the riding component
@@ -26,14 +28,18 @@
var/key_type
///The inserted key, needed on some vehicles to start the engine
var/obj/item/key/inserted_key
/// Whether the vehicle is currently able to move
var/can_move = TRUE
var/list/autogrant_actions_passenger //plain list of typepaths
var/list/autogrant_actions_controller //assoc list "[bitflag]" = list(typepaths)
var/list/mob/occupant_actions //assoc list mob = list(type = action datum assigned to mob)
///This vehicle will follow us when we move (like atrailer duh)
var/obj/tgvehicle/trailer
var/are_legs_exposed = FALSE
/// Sound played when entering a vehicle
var/enter_sound
/// Sound played when exiting a vehicle
var/exit_sound
new_attack_chain = TRUE
/obj/tgvehicle/Initialize(mapload)
. = ..()
@@ -51,6 +57,8 @@
/obj/tgvehicle/Exited(atom/movable/gone, direction)
if(gone == inserted_key)
inserted_key = null
if(exit_sound)
playsound(src, exit_sound, 70, TRUE)
return ..()
/obj/tgvehicle/examine(mob/user)
@@ -137,7 +145,7 @@
return
/obj/tgvehicle/relaymove(mob/living/user, direction)
if(!can_move)
if(!canmove)
return FALSE
if(is_driver(user))
return relaydrive(user, direction)
@@ -192,7 +200,7 @@
add_occupant(M)
return ..()
/obj/tgvehicle/attackby__legacy__attackchain(obj/item/I, mob/user, params)
/obj/tgvehicle/item_interaction(mob/living/user, obj/item/I, list/modifiers)
if(!key_type || is_key(inserted_key) || !is_key(I))
return ..()
if(user.drop_item())
@@ -203,15 +211,12 @@
inserted_key = I
else
to_chat(user, "<span class='warning'>[I] seems to be stuck to your hand!</span>")
if(inserted_key) //just in case there's an invalid key
inserted_key.forceMove(drop_location())
inserted_key = I
/obj/tgvehicle/AltClick(mob/user)
if(!inserted_key)
return ..()
if(!is_occupant(user))
to_chat(user, "<span class='warning'>You must be riding the [src] to remove [src]'s key!</span>")
if(!is_occupant(user) || !(occupants[user] & VEHICLE_CONTROL_DRIVE))
to_chat(user, "<span class='warning'>You must be driving the [src] to remove [src]'s key!</span>")
return
to_chat(user, "<span class='notice'>You remove [inserted_key] from [src].</span>")
inserted_key.forceMove(drop_location())
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 88 KiB

+3
View File
@@ -3240,8 +3240,11 @@
#include "code\modules\vehicle\sportscar.dm"
#include "code\modules\vehicle\vehicle.dm"
#include "code\modules\vehicle\tg_vehicles\scooter.dm"
#include "code\modules\vehicle\tg_vehicles\sealed.dm"
#include "code\modules\vehicle\tg_vehicles\tg_vehicle_actions.dm"
#include "code\modules\vehicle\tg_vehicles\tg_vehicles.dm"
#include "code\modules\vehicle\tg_vehicles\cars\car.dm"
#include "code\modules\vehicle\tg_vehicles\cars\clowncar.dm"
#include "code\modules\virology\virology_goals.dm"
#include "code\modules\vote\vote_datum.dm"
#include "code\modules\vote\vote_presets.dm"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.