Overhaul of how fire is handled, this is where a lot of the lag came from. Also the zas neutering things are included.

This commit is contained in:
d3athrow
2013-07-09 04:43:58 -05:00
parent 87f076eb61
commit 9aca107200
5 changed files with 220 additions and 172 deletions

View File

@@ -56,8 +56,9 @@ mob/proc/airflow_stun()
if(!(status_flags & CANSTUN) && !(status_flags & CANWEAKEN))
src << "\blue You stay upright as the air rushes past you."
return 0
if(weakened <= 0) src << "\red The sudden rush of air knocks you over!"
weakened = max(weakened,5)
/* if(weakened <= 0) src << "\red The sudden rush of air knocks you over!"
weakened = max(weakened,5)*/
src << "\blue You stay upright as the air rushes past you."
last_airflow_stun = world.time
mob/living/silicon/airflow_stun()
@@ -74,8 +75,10 @@ mob/living/carbon/human/airflow_stun()
if(!(status_flags & CANSTUN) && !(status_flags & CANWEAKEN))
src << "\blue You stay upright as the air rushes past you."
return 0
/*
if(weakened <= 0) src << "\red The sudden rush of air knocks you over!"
weakened = max(weakened,rand(1,5))
weakened = max(weakened,rand(1,5))*/
src << "\blue You stay upright as the air rushes past you."
last_airflow_stun = world.time
atom/movable/proc/check_airflow_movable(n)
@@ -143,7 +146,7 @@ proc/Airflow(zone/A, zone/B)
air_repelled = temporary_pplz
for(var/atom/movable/M in air_sucked)
if(1) break
if(M.last_airflow > world.time - vsc.airflow_delay) continue
//Check for knocking people over
@@ -202,7 +205,7 @@ proc/AirflowSpace(zone/A)
var/list/pplz = A.movables() //We only need to worry about things in the zone, not things in space.
for(var/atom/movable/M in pplz)
if(1) break
if(M.last_airflow > world.time - vsc.airflow_delay) continue
if(ismob(M) && n > vsc.airflow_stun_pressure)
@@ -232,6 +235,7 @@ atom/movable
var/tmp/last_airflow = 0
proc/GotoAirflowDest(n)
if(1) return
if(!airflow_dest) return
if(airflow_speed < 0) return
if(last_airflow > world.time - vsc.airflow_delay) return
@@ -297,6 +301,7 @@ atom/movable
proc/RepelAirflowDest(n)
if(1) return
if(!airflow_dest) return
if(airflow_speed < 0) return
if(last_airflow > world.time - vsc.airflow_delay) return
@@ -370,7 +375,7 @@ mob/airflow_hit(atom/A)
for(var/mob/M in hearers(src))
M.show_message("\red <B>\The [src] slams into \a [A]!</B>",1,"\red You hear a loud slam!",2)
playsound(src.loc, "smash.ogg", 25, 1, -1)
weakened = max(weakened, (istype(A,/obj/item) ? A:w_class : rand(1,5))) //Heheheh
//weakened = max(weakened, (istype(A,/obj/item) ? A:w_class : rand(1,5))) //Heheheh
. = ..()
obj/airflow_hit(atom/A)
@@ -402,12 +407,13 @@ mob/living/carbon/human/airflow_hit(atom/A)
blocked = run_armor_check("groin","melee")
apply_damage(b_loss/3, BRUTE, "groin", blocked, 0, "Airflow")
/*
if(airflow_speed > 10)
paralysis += round(airflow_speed * vsc.airflow_stun)
stunned = max(stunned,paralysis + 3)
else
stunned += round(airflow_speed * vsc.airflow_stun/2)
*/
. = ..()
zone/proc/movables()

View File

@@ -75,21 +75,31 @@ connection
var/list/connections = air_master.turfs_with_connections[ref_A]
connections.Remove(src)
//Ensure we delete the right values by sanity checkign right before deletion.
Sanitize()
//Remove connection from current zones.
//Remove connection from zones.
if(A)
if(A.zone && A.zone.connections)
A.zone.connections.Remove(src)
if(!A.zone.connections.len)
A.zone.connections = null
if(istype(zone_A) && (!A || A.zone != zone_A))
if(zone_A.connections)
zone_A.connections.Remove(src)
if(!zone_A.connections.len)
zone_A.connections = null
if(B)
if(B.zone && B.zone.connections)
B.zone.connections.Remove(src)
if(!B.zone.connections.len)
B.zone.connections = null
if(istype(zone_B) && (!B || B.zone != zone_B))
if(zone_B.connections)
zone_B.connections.Remove(src)
if(!zone_B.connections.len)
zone_B.connections = null
//Disconnect zones while handling unusual conditions.
// e.g. loss of a zone on a turf
if(A && A.zone && B && B.zone)

View File

@@ -20,20 +20,19 @@ turf/proc/hotspot_expose(exposed_temperature, exposed_volume, soh = 0)
turf/simulated/hotspot_expose(exposed_temperature, exposed_volume, soh)
if(fire_protection > world.time-300) return
if(fire_protection > world.time-300)
return 0
if(locate(/obj/fire) in src)
return 1
var/datum/gas_mixture/air_contents = return_air()
if(!air_contents || exposed_temperature < PLASMA_MINIMUM_BURN_TEMPERATURE)
return 0
var/igniting = 0
if(locate(/obj/fire) in src)
return 1
var/datum/gas/volatile_fuel/fuel = locate() in air_contents.trace_gases
var/obj/effect/decal/cleanable/liquid_fuel/liquid = locate() in src
if(air_contents.calculate_firelevel(liquid) > vsc.IgnitionLevel && (fuel || liquid || air_contents.toxins > 0.5))
if(air_contents.check_combustability(liquid))
igniting = 1
if(air_contents.oxygen < 0.5)
return 0
if(! (locate(/obj/fire) in src))
@@ -64,51 +63,38 @@ obj
process()
. = 1
if(firelevel > vsc.IgnitionLevel)
//get location and check if it is in a proper ZAS zone
var/turf/simulated/floor/S = loc
if(!S.zone) del src //Cannot exist where zones are broken.
if(!S.zone)
del src
if(istype(S))
var
datum/gas_mixture/air_contents = S.return_air()
//Get whatever trace fuels are in the area
datum/gas/volatile_fuel/fuel = locate() in air_contents.trace_gases
//Also get liquid fuels on the ground.
obj/effect/decal/cleanable/liquid_fuel/liquid = locate() in S
if(!istype(S))
del src
var/datum/gas_mixture/flow = air_contents.remove_ratio(vsc.fire_consuption_rate)
//The reason we're taking a part of the air instead of all of it is so that it doesn't jump to
//the fire's max temperature instantaneously.
var/datum/gas_mixture/air_contents = S.return_air()
//get liquid fuels on the ground.
var/obj/effect/decal/cleanable/liquid_fuel/liquid = locate() in S
//and the volatile stuff from the air
var/datum/gas/volatile_fuel/fuel = locate() in air_contents.trace_gases
//since the air is processed in fractions, we need to make sure not to have any minuscle residue or
//the amount of moles might get to low for some functions to catch them and thus result in wonky behaviour
if(air_contents.oxygen < 0.001)
air_contents.oxygen = 0
if(air_contents.toxins < 0.001)
air_contents.toxins = 0
if(fuel)
if(fuel.moles < 0.001)
air_contents.trace_gases.Remove(fuel)
//check if there is something to combust
if(!air_contents.check_combustability(liquid))
//del src
RemoveFire()
//get a firelevel and set the icon
firelevel = air_contents.calculate_firelevel(liquid)
//Ensure that there is an appropriate amount of fuel and O2 here.
if(firelevel > 0.25 && flow.oxygen > 0.3 && (air_contents.toxins || fuel || liquid))
for(var/direction in cardinal)
if(S.air_check_directions&direction) //Grab all valid bordering tiles
var/turf/simulated/enemy_tile = get_step(S, direction)
if(istype(enemy_tile))
//If extinguisher mist passed over the turf it's trying to spread to, don't spread and
//reduce firelevel.
if(enemy_tile.fire_protection > world.time-30)
firelevel -= 1.5
continue
//Spread the fire.
if(!(locate(/obj/fire) in enemy_tile))
if( prob( firelevel*10 ) && S.CanPass(null, enemy_tile, 0,0) && enemy_tile.CanPass(null, S, 0,0))
new/obj/fire(enemy_tile,firelevel)
if(flow)
//Ensure adequate oxygen and fuel.
if(flow.oxygen > 0.3 && (flow.toxins || fuel || liquid))
//Change icon depending on the fuel, and thus temperature.
if(firelevel > 6)
icon_state = "3"
SetLuminosity(7)
@@ -119,31 +105,53 @@ obj
icon_state = "1"
SetLuminosity(3)
//Ensure flow temperature is higher than minimum fire temperatures.
flow.temperature = max(PLASMA_MINIMUM_BURN_TEMPERATURE+0.1,flow.temperature)
//Burn the gas mixture.
flow.zburn(liquid)
if(fuel && fuel.moles <= 0.00001)
air_contents.trace_gases.Remove(fuel)
else
del src
S.assume_air(flow) //Then put it back where you found it.
else
del src
else
del src
else
del src
//im not sure how to implement a version that works for every creature so for now monkeys are firesafe
for(var/mob/living/carbon/human/M in loc)
M.FireBurn(firelevel) //Burn the humans!
M.FireBurn(firelevel, air_contents.temperature, air_contents.return_pressure() ) //Burn the humans!
/spread
for(var/direction in cardinal)
if(S.air_check_directions&direction) //Grab all valid bordering tiles
var/turf/simulated/enemy_tile = get_step(S, direction)
if(istype(enemy_tile))
var/datum/gas_mixture/acs = enemy_tile.return_air()
var/obj/effect/decal/cleanable/liquid_fuel/liq = locate() in enemy_tile
if(!acs.check_combustability(liq))
return
//If extinguisher mist passed over the turf it's trying to spread to, don't spread and
//reduce firelevel.
if(enemy_tile.fire_protection > world.time-30)
firelevel -= 1.5
continue
//Spread the fire.
if(!(locate(/obj/fire) in enemy_tile))
if( prob( 50 + 50 * (firelevel/vsc.fire_firelevel_multiplier) ) && S.CanPass(null, enemy_tile, 0,0) && enemy_tile.CanPass(null, S, 0,0))
new/obj/fire(enemy_tile,firelevel)
//seperate part of the present gas
//this is done to prevent the fire burning all gases in a single pass
var/datum/gas_mixture/flow = air_contents.remove_ratio(vsc.fire_consuption_rate)
///////////////////////////////// FLOW HAS BEEN CREATED /// DONT DELETE THE FIRE UNTIL IT IS MERGED BACK OR YOU WILL DELETE AIR ///////////////////////////////////////////////
if(flow)
if(flow.check_combustability(liquid))
//Ensure flow temperature is higher than minimum fire temperatures.
//this creates some energy ex nihilo but is necessary to get a fire started
//lets just pretend this energy comes from the ignition source and dont mention this again
//flow.temperature = max(PLASMA_MINIMUM_BURN_TEMPERATURE+0.1,flow.temperature)
//burn baby burn!
flow.zburn(liquid,1)
//merge the air back
S.assume_air(flow)
///////////////////////////////// FLOW HAS BEEN REMERGED /// feel free to delete the fire again from here on //////////////////////////////////////////////////////////////////
New(newLoc,fl)
@@ -167,6 +175,12 @@ obj
..()
proc/RemoveFire()
if (istype(loc, /turf/simulated))
SetLuminosity(0)
loc = null
air_master.active_hotspots.Remove(src)
turf/simulated/var/fire_protection = 0 //Protects newly extinguished tiles from being overrun again.
@@ -175,13 +189,14 @@ turf/simulated/apply_fire_protection()
fire_protection = world.time
datum/gas_mixture/proc/zburn(obj/effect/decal/cleanable/liquid_fuel/liquid)
//This proc is similar to fire(), but uses a simple logarithm to calculate temp, and is thus more stable with ZAS.
if(temperature > PLASMA_MINIMUM_BURN_TEMPERATURE)
var
total_fuel = toxins
datum/gas_mixture/proc/zburn(obj/effect/decal/cleanable/liquid_fuel/liquid,force_burn)
var/value = 0
datum/gas/volatile_fuel/fuel = locate() in trace_gases
if((temperature > PLASMA_MINIMUM_BURN_TEMPERATURE || force_burn) && check_combustability(liquid) )
var/total_fuel = 0
var/datum/gas/volatile_fuel/fuel = locate() in trace_gases
total_fuel += toxins
if(fuel)
//Volatile Fuel
@@ -192,65 +207,93 @@ datum/gas_mixture/proc/zburn(obj/effect/decal/cleanable/liquid_fuel/liquid)
if(liquid.amount <= 0)
del liquid
else
total_fuel += liquid.amount*15
if(! (fuel || toxins || liquid) )
return 0 //If there's no fuel, there's no burn. Can't divide by zero anyway.
if(oxygen > 0.3)
total_fuel += liquid.amount
//Calculate the firelevel.
var/firelevel = calculate_firelevel(liquid)
//Reaches a maximum practical temperature of around 4500.
//get the current inner energy of the gas mix
//this must be taken here to prevent the addition or deletion of energy by a changing heat capacity
var/starting_energy = temperature * heat_capacity()
//Increase temperature.
temperature = max( vsc.fire_temperature_multiplier*log(0.04*firelevel + 1.24) , temperature )
//determine the amount of oxygen used
var/total_oxygen = min(oxygen, 2 * total_fuel)
var/total_reactants = min(oxygen, 2*total_fuel) + total_fuel
//determine the amount of fuel actually used
var/used_fuel_ratio = min(oxygen / 2 , total_fuel) / total_fuel
total_fuel = total_fuel * used_fuel_ratio
//Consume some gas.
var/consumed_gas = max( min( total_reactants, vsc.fire_gas_combustion_ratio*firelevel ), 0.2)
var/total_reactants = total_fuel + total_oxygen
oxygen -= min(oxygen, (total_reactants-total_fuel)*consumed_gas/total_reactants )
//determine the amount of reactants actually reacting
var/used_reactants_ratio = min( max(total_reactants * firelevel / vsc.fire_firelevel_multiplier, 0.2), total_reactants) / total_reactants
toxins -= min(toxins, toxins*consumed_gas/total_reactants )
//remove and add gasses as calculated
oxygen -= min(oxygen, total_oxygen * used_reactants_ratio )
carbon_dioxide += max(consumed_gas, 0)
toxins -= min(toxins, toxins * used_fuel_ratio * used_reactants_ratio )
carbon_dioxide += max(2 * total_fuel, 0)
if(fuel)
fuel.moles -= fuel.moles*consumed_gas/total_reactants
fuel.moles -= fuel.moles * used_fuel_ratio * used_reactants_ratio
if(fuel.moles <= 0) del fuel
if(liquid)
liquid.amount -= liquid.amount*consumed_gas/(total_reactants)
liquid.amount -= liquid.amount * used_fuel_ratio * used_reactants_ratio
if(liquid.amount <= 0) del liquid
//calculate the energy produced by the reaction and then set the new temperature of the mix
temperature = (starting_energy + vsc.fire_fuel_energy_release * total_fuel) / heat_capacity()
update_values()
return consumed_gas
return 0
value = total_reactants * used_reactants_ratio
return value
datum/gas_mixture/proc/check_combustability(obj/effect/decal/cleanable/liquid_fuel/liquid)
//this check comes up very often and is thus centralized here to ease adding stuff
var/datum/gas/volatile_fuel/fuel = locate() in trace_gases
var/value = 0
if(oxygen && (toxins || fuel || liquid))
value = 1
return value
datum/gas_mixture/proc/calculate_firelevel(obj/effect/decal/cleanable/liquid_fuel/liquid)
//Calculates the firelevel based on one equation instead of having to do this multiple times in different areas.
var
datum/gas/volatile_fuel/fuel = locate() in trace_gases
var/total_fuel = toxins - 0.5
var/datum/gas/volatile_fuel/fuel = locate() in trace_gases
var/total_fuel = 0
var/firelevel = 0
if(check_combustability(liquid))
total_fuel += toxins
if(liquid)
total_fuel += (liquid.amount*15)
total_fuel += liquid.amount
if(fuel)
total_fuel += fuel.moles
var/total_combustables = (total_fuel + oxygen)
if(total_fuel <= 0 || oxygen <= 0)
return 0
return max( 0, vsc.fire_firelevel_multiplier*(total_combustables/(total_combustables + nitrogen))*log(2*total_combustables/oxygen)*log(total_combustables/total_fuel))
if(total_fuel > 0 && oxygen > 0)
//slows down the burning when the concentration of the reactants is low
var/dampening_multiplier = total_combustables / (total_combustables + nitrogen + carbon_dioxide)
//calculates how close the mixture of the reactants is to the optimum
var/mix_multiplier = 1 / (1 + (5 * ((oxygen / total_combustables) ^2)))
//toss everything together
firelevel = vsc.fire_firelevel_multiplier * mix_multiplier * dampening_multiplier
return max( 0, firelevel)
/mob/living/carbon/human/proc/FireBurn(var/firelevel)
/mob/living/carbon/human/proc/FireBurn(var/firelevel, var/last_temperature, var/pressure)
// mostly using the old proc from Sky until I can think of something better
//Burns mobs due to fire. Respects heat transfer coefficients on various body parts.
//Due to TG reworking how fireprotection works, this is kinda less meaningful.
@@ -261,8 +304,9 @@ datum/gas_mixture/proc/calculate_firelevel(obj/effect/decal/cleanable/liquid_fue
legs_exposure = 1
arms_exposure = 1
var/mx = min(max(0.1,firelevel / 20),10)
var/last_temperature = vsc.fire_temperature_multiplier*log(0.04*firelevel + 1.24)
//determine the multiplier
//minimize this for low-pressure enviroments
var/mx = 5 * firelevel/vsc.fire_firelevel_multiplier * min(pressure / ONE_ATMOSPHERE, 1)
//Get heat transfer coefficients for clothing.
//skytodo: kill anyone who breaks things then orders me to fix them
@@ -293,4 +337,3 @@ datum/gas_mixture/proc/calculate_firelevel(obj/effect/decal/cleanable/liquid_fue
apply_damage(0.4*mx*arms_exposure, BURN, "r_arm", 0, 0, "Fire")
//flash_pain()
#undef ZAS_FIRE_CONSUMPTION_RATE

View File

@@ -1,71 +1,64 @@
var/global/vs_control/vsc = new
vs_control/var
IgnitionLevel = 0.5 //default 0.5
IgnitionLevel_DESC = "Determines point at which fire can ignite"
fire_consuption_rate = 0.025 //default .25
fire_consuption_rate = 0.25
fire_consuption_rate_NAME = "Fire - Air Consumption Ratio"
fire_consuption_rate_DESC = "Ratio of air removed and combusted per tick."
fire_firelevel_multiplier = 80 //default 25
fire_firelevel_multiplier = 25
fire_firelevel_multiplier_NAME = "Fire - Firelevel Constant"
fire_firelevel_multiplier_DESC = "Multiplied by the equation for firelevel, affects the combustion and ignition of gas mixes."
fire_firelevel_multiplier_DESC = "Multiplied by the equation for firelevel, affects mainly the extingiushing of fires."
fire_temperature_multiplier = 10000 //default 1700
fire_temperature_multiplier_NAME = "Fire - Temperature Multiplier"
fire_temperature_multiplier_DESC = "Base value for fire temperatures."
fire_gas_combustion_ratio = 0.1 //default .25
fire_gas_combustion_ratio_NAME = "Fire - Gas Conversion Ratio"
fire_gas_combustion_ratio_DESC = "The rate at which oxygen and plasma are converted to CO2, expressed in terms of the firelevel."
fire_fuel_energy_release = 797000
fire_fuel_energy_release_NAME = "Fire - Fuel energy release"
fire_fuel_energy_release_DESC = "The energy in joule released when burning one mol of a burnable substance"
airflow_lightest_pressure = 25 //default 25
airflow_lightest_pressure = 20
airflow_lightest_pressure_NAME = "Airflow - Small Movement Threshold %"
airflow_lightest_pressure_DESC = "Percent of 1 Atm. at which items with the small weight classes will move."
airflow_light_pressure = 35 //default 35
airflow_light_pressure = 35
airflow_light_pressure_NAME = "Airflow - Medium Movement Threshold %"
airflow_light_pressure_DESC = "Percent of 1 Atm. at which items with the medium weight classes will move."
airflow_medium_pressure = 55 //default 50
airflow_medium_pressure = 50
airflow_medium_pressure_NAME = "Airflow - Heavy Movement Threshold %"
airflow_medium_pressure_DESC = "Percent of 1 Atm. at which items with the largest weight classes will move."
airflow_heavy_pressure = 70 //default 65
airflow_heavy_pressure = 65
airflow_heavy_pressure_NAME = "Airflow - Mob Movement Threshold %"
airflow_heavy_pressure_DESC = "Percent of 1 Atm. at which mobs will move."
airflow_dense_pressure = 80 //default 85
airflow_dense_pressure = 85
airflow_dense_pressure_NAME = "Airflow - Dense Movement Threshold %"
airflow_dense_pressure_DESC = "Percent of 1 Atm. at which items with canisters and closets will move."
airflow_stun_pressure = 60 //default 60
airflow_stun_pressure = 60
airflow_stun_pressure_NAME = "Airflow - Mob Stunning Threshold %"
airflow_stun_pressure_DESC = "Percent of 1 Atm. at which mobs will be stunned by airflow."
airflow_stun_cooldown = 60 //default 60
airflow_stun_cooldown = 60
airflow_stun_cooldown_NAME = "Aiflow Stunning - Cooldown"
airflow_stun_cooldown_DESC = "How long, in tenths of a second, to wait before stunning them again."
airflow_stun = 0.75 //default 1
airflow_stun = 1
airflow_stun_NAME = "Airflow Impact - Stunning"
airflow_stun_DESC = "How much a mob is stunned when hit by an object."
airflow_damage = 2 //default 2
airflow_damage = 2
airflow_damage_NAME = "Airflow Impact - Damage"
airflow_damage_DESC = "Damage from airflow impacts."
airflow_speed_decay = 1.5 //default 1.5
airflow_speed_decay = 1.5
airflow_speed_decay_NAME = "Airflow Speed Decay"
airflow_speed_decay_DESC = "How rapidly the speed gained from airflow decays."
airflow_delay = 45 //default 30
airflow_delay = 30
airflow_delay_NAME = "Airflow Retrigger Delay"
airflow_delay_DESC = "Time in deciseconds before things can be moved by airflow again."
airflow_mob_slowdown = 1 //default 1
airflow_mob_slowdown = 1
airflow_mob_slowdown_NAME = "Airflow Slowdown"
airflow_mob_slowdown_DESC = "Time in tenths of a second to add as a delay to each movement by a mob if they are fighting the pull of the airflow."
@@ -229,7 +222,6 @@ vs_control
plc.CONTAMINATION_LOSS = 0.075
if("ZAS - Normal")
IgnitionLevel = 0.5
airflow_lightest_pressure = 20
airflow_light_pressure = 35
airflow_medium_pressure = 50
@@ -244,7 +236,6 @@ vs_control
airflow_mob_slowdown = 1
if("ZAS - Forgiving")
IgnitionLevel = 1
airflow_lightest_pressure = 45
airflow_light_pressure = 60
airflow_medium_pressure = 120
@@ -259,7 +250,6 @@ vs_control
airflow_mob_slowdown = 0
if("ZAS - Dangerous")
IgnitionLevel = 0.4
airflow_lightest_pressure = 15
airflow_light_pressure = 30
airflow_medium_pressure = 45
@@ -274,7 +264,6 @@ vs_control
airflow_mob_slowdown = 2
if("ZAS - Hellish")
IgnitionLevel = 0.3
airflow_lightest_pressure = 20
airflow_light_pressure = 30
airflow_medium_pressure = 40

View File

@@ -293,7 +293,7 @@ zone/proc/process()
//Air Movement//
////////////////
var/list/sharing_lookup_table = list(0.15, 0.20, 0.24, 0.27, 0.30, 0.33)
var/list/sharing_lookup_table = list(0.03, 0.07, 0.11, 0.15, 0.18, 0.20)
proc/ShareRatio(datum/gas_mixture/A, datum/gas_mixture/B, connecting_tiles)
//Shares a specific ratio of gas between mixtures using simple weighted averages.
@@ -385,7 +385,7 @@ proc/ShareSpace(datum/gas_mixture/A, list/unsimulated_tiles, dbg_output)
// slowly than small rooms, preserving our good old "hollywood-style"
// oh-shit effect when large rooms get breached, but still having small
// rooms remain pressurized for long enough to make escape possible.
share_size = max(1, max(size - 5, 1) + unsimulated_tiles.len)
share_size = max(1, max(size + 3, 1) + unsimulated_tiles.len)
correction_ratio = share_size / unsimulated_tiles.len
for(var/turf/T in unsimulated_tiles)