noita_entangled_worlds/quant.ew/files/system/enemy_sync.lua

316 lines
13 KiB
Lua
Raw Normal View History

2024-08-07 17:11:55 +03:00
local util = dofile_once("mods/quant.ew/files/core/util.lua")
local ctx = dofile_once("mods/quant.ew/files/core/ctx.lua")
local net = dofile_once("mods/quant.ew/files/core/net.lua")
local player_fns = dofile_once("mods/quant.ew/files/core/player_fns.lua")
local np = require("noitapatcher")
2024-05-30 12:41:56 +03:00
local rpc = net.new_rpc_namespace()
2024-05-08 20:33:41 +03:00
local enemy_sync = {}
local dead_entities = {}
local confirmed_kills = {}
np.CrossCallAdd("ew_es_death_notify", function(enemy_id, responsible_id)
local player_data = player_fns.get_player_data_by_local_entity_id(responsible_id)
local responsible = nil
if player_data ~= nil then
responsible = player_data.peer_id
else
responsible = responsible_id
end
table.insert(dead_entities, {enemy_id, responsible})
end)
2024-05-08 20:33:41 +03:00
local function world_exists_for(entity)
local x, y = EntityGetFirstHitboxCenter(entity)
local w, h = 5, 5 -- TODO
w = w * 0.5
h = h * 0.5
return DoesWorldExistAt(x - w, y - h, x + w, y + h)
end
2024-05-09 23:37:50 +03:00
local function table_extend(to, from)
for _, e in ipairs(from) do
to[#to+1] = e
end
end
local function table_extend_filtered(to, from, filter)
for _, e in ipairs(from) do
if filter(e) then
to[#to+1] = e
end
end
end
2024-05-10 10:30:14 +03:00
local function get_sync_entities()
2024-06-14 20:40:38 +03:00
local entities = {} -- TODO maybe only sync those close to players?
table_extend_filtered(entities, EntityGetWithTag("enemy"), function (ent)
return not (EntityHasTag(ent, "ew_no_enemy_sync"))
end)
2024-06-19 13:12:06 +03:00
table_extend(entities, EntityGetWithTag("ew_enemy_sync_extra"))
2024-08-13 14:18:45 +03:00
table_extend_filtered(entities, EntityGetWithTag("prop_physics"), function (ent)
return constants.phys_sync_allowed[EntityGetFilename(ent)]
end)
-- table_extend_filtered(entities, EntityGetWithTag("projectile"), function (ent)
-- return not (EntityHasTag(ent, "ew_no_enemy_sync") or EntityHasTag(ent, "projectile_player"))
-- end)
2024-05-10 10:30:14 +03:00
return entities
end
function enemy_sync.host_upload_entities()
local entities = get_sync_entities()
2024-05-08 20:33:41 +03:00
local enemy_data_list = {}
2024-05-09 23:37:50 +03:00
for i, enemy_id in ipairs(entities) do
2024-05-08 20:33:41 +03:00
if not world_exists_for(enemy_id) then
goto continue
end
local filename = EntityGetFilename(enemy_id)
local x, y = EntityGetTransform(enemy_id)
2024-05-09 23:37:50 +03:00
local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
2024-05-08 22:13:17 +03:00
local vx, vy = 0, 0
2024-05-09 23:37:50 +03:00
if character_data ~= nil then
2024-05-08 22:13:17 +03:00
vx, vy = ComponentGetValue2(character_data, "mVelocity")
end
2024-05-09 23:37:50 +03:00
local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "AnimalAIComponent")
2024-06-13 21:30:19 +03:00
if ai_component ~= 0 and ai_component ~= nil then
2024-05-09 23:37:50 +03:00
ComponentSetValue2(ai_component, "max_distance_to_cam_to_start_hunting", math.pow(2, 29))
end
local hp, max_hp, has_hp = util.get_ent_health(enemy_id)
2024-08-13 14:18:45 +03:00
local phys_info = nil
local phys_component = EntityGetFirstComponent(enemy_id, "PhysicsBody2Component")
if phys_component ~= nil and phys_component ~= 0 then
local initialized = ComponentGetValue2(phys_component, "mInitialized")
if initialized then
phys_info = {np.PhysBodyGetTransform(phys_component)}
end
end
if has_hp then
util.ensure_component_present(enemy_id, "LuaComponent", "ew_death_notify", {
2024-08-07 17:21:25 +03:00
script_death = "mods/quant.ew/files/resource/cbs/death_notify.lua"
})
end
2024-05-12 18:05:25 +03:00
-- TODO: figure out how to sync those.
-- local laser_sight_data = nil
-- local laser_sight = EntityGetFirstComponentIncludingDisabled(enemy_id, "SpriteComponent", "laser_sight")
-- if laser_sight ~= nil and laser_sight ~= 0 then
-- -- local x, y, r =
-- end
2024-08-13 14:18:45 +03:00
table.insert(enemy_data_list, {enemy_id, filename, x, y, vx, vy, hp, max_hp, phys_info})
2024-05-08 20:33:41 +03:00
::continue::
end
2024-05-30 12:41:56 +03:00
rpc.handle_enemy_data(enemy_data_list)
if #dead_entities > 0 then
rpc.handle_death_data(dead_entities)
end
dead_entities = {}
2024-05-08 20:33:41 +03:00
end
function enemy_sync.client_cleanup()
2024-05-10 10:30:14 +03:00
local entities = get_sync_entities()
for i, enemy_id in ipairs(entities) do
2024-05-08 20:33:41 +03:00
if not EntityHasTag(enemy_id, "ew_replicated") then
2024-06-19 13:12:06 +03:00
local filename = EntityGetFilename(enemy_id)
print("Despawning unreplicated "..enemy_id.." "..filename)
2024-05-08 20:33:41 +03:00
EntityKill(enemy_id)
end
end
local frame = GameGetFrameNum()
2024-05-10 10:30:14 +03:00
for remote_id, enemy_data in pairs(ctx.entity_by_remote_id) do
if frame - enemy_data.frame > 60*1 then
print("Despawning stale "..remote_id.." "..enemy_data.id)
2024-05-08 20:33:41 +03:00
EntityKill(enemy_data.id)
2024-05-10 10:30:14 +03:00
ctx.entity_by_remote_id[remote_id] = nil
2024-05-08 20:33:41 +03:00
end
end
end
2024-05-30 12:41:56 +03:00
function enemy_sync.on_world_update_host()
if GameGetFrameNum() % 2 == 1 then
enemy_sync.host_upload_entities()
end
end
function enemy_sync.on_world_update_client()
if GameGetFrameNum() % 20 == 1 then
enemy_sync.client_cleanup()
end
end
rpc.opts_reliable()
function rpc.handle_death_data(death_data)
for _, remote_data in ipairs(death_data) do
local remote_id = remote_data[1]
if confirmed_kills[remote_id] then
GamePrint("Remote id has been killed already..?")
goto continue
end
confirmed_kills[remote_id] = true
local responsible_entity = 0
local peer_data = player_fns.peer_get_player_data(remote_data[2], true)
if peer_data ~= nil then
responsible_entity = peer_data.entity
elseif ctx.entity_by_remote_id[remote_data[2]] ~= nil then
responsible_entity = ctx.entity_by_remote_id[remote_data[2]]
end
local enemy_data = ctx.entity_by_remote_id[remote_id]
if enemy_data ~= nil and EntityGetIsAlive(enemy_data.id) then
local enemy_id = enemy_data.id
local immortal = EntityGetFirstComponentIncludingDisabled(enemy_id, "LuaComponent", "ew_immortal")
2024-06-09 19:55:07 +03:00
if immortal ~= 0 then
EntityRemoveComponent(enemy_id, immortal)
end
local protection_component_id = GameGetGameEffect(enemy_id, "PROTECTION_ALL")
if protection_component_id ~= 0 then
EntitySetComponentIsEnabled(enemy_id, protection_component_id, false)
end
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
end
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp
if dmg > 0 then
EntityInflictDamage(enemy_id, dmg+0.1, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity)
end
EntityInflictDamage(enemy_id, 1000000000, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity) -- Just to be sure
EntityKill(enemy_id)
end
::continue::
end
end
2024-05-30 12:41:56 +03:00
function rpc.handle_enemy_data(enemy_data)
2024-05-08 20:33:41 +03:00
-- GamePrint("Got enemy data")
for _, enemy_info_raw in ipairs(enemy_data) do
local remote_enemy_id = enemy_info_raw[1]
local filename = enemy_info_raw[2]
local x = enemy_info_raw[3]
local y = enemy_info_raw[4]
2024-05-08 22:13:17 +03:00
local vx = enemy_info_raw[5]
local vy = enemy_info_raw[6]
local hp = enemy_info_raw[7]
local max_hp = enemy_info_raw[8]
2024-08-13 14:18:45 +03:00
local phys_info = enemy_info_raw[9]
local has_died = filename == nil
2024-05-08 20:33:41 +03:00
local frame = GameGetFrameNum()
if confirmed_kills[remote_enemy_id] then
goto continue
end
2024-05-10 10:30:14 +03:00
if ctx.entity_by_remote_id[remote_enemy_id] ~= nil and not EntityGetIsAlive(ctx.entity_by_remote_id[remote_enemy_id].id) then
ctx.entity_by_remote_id[remote_enemy_id] = nil
2024-05-08 20:33:41 +03:00
end
2024-05-10 22:17:45 +03:00
2024-05-10 10:30:14 +03:00
if ctx.entity_by_remote_id[remote_enemy_id] == nil then
if filename == nil then
goto continue
end
local enemy_id = util.load_ephemerial(filename, x, y)
2024-05-12 18:05:25 +03:00
EntityAddTag(enemy_id, "ew_replicated")
EntityAddTag(enemy_id, "polymorphable_NOT")
2024-08-07 17:21:25 +03:00
EntityAddComponent2(enemy_id, "LuaComponent", {_tags="ew_immortal", script_damage_about_to_be_received = "mods/quant.ew/files/resource/cbs/immortal.lua"})
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
end
for _, name in ipairs({"AnimalAIComponent", "PhysicsAIComponent", "PhysicsBodyComponent", "CameraBoundComponent"}) do
2024-06-09 19:55:07 +03:00
local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, name)
if ai_component ~= 0 then
EntityRemoveComponent(enemy_id, ai_component)
end
2024-05-08 22:13:17 +03:00
end
2024-05-10 10:30:14 +03:00
ctx.entity_by_remote_id[remote_enemy_id] = {id = enemy_id, frame = frame}
local phys_component = EntityGetFirstComponent(enemy_id, "PhysicsBody2Component")
if phys_component ~= nil and phys_component ~= 0 then
ComponentSetValue2(phys_component, "destroy_body_if_entity_destroyed", true)
end
-- Make sure stuff doesn't decide to explode on clients by itself.
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_damage_percent", 0)
ComponentSetValue2(expl_component, "physics_body_modified_death_probability", 0)
ComponentSetValue2(expl_component, "explode_on_death_percent", 1)
end
2024-05-08 20:33:41 +03:00
end
2024-05-10 10:30:14 +03:00
local enemy_data = ctx.entity_by_remote_id[remote_enemy_id]
2024-05-08 20:33:41 +03:00
enemy_data.frame = frame
local enemy_id = enemy_data.id
2024-05-08 22:13:17 +03:00
2024-08-13 14:18:45 +03:00
local phys_component = EntityGetFirstComponent(enemy_id, "PhysicsBody2Component")
if phys_component ~= nil and phys_component ~= 0 and phys_info ~= nil then
-- A physics body doesn't exist otherwise, causing a crash
local initialized = ComponentGetValue2(phys_component, "mInitialized")
if initialized then
np.PhysBodySetTransform(phys_component, unpack(phys_info))
end
end
if not has_died then
local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "CharacterDataComponent")
if character_data ~= nil then
ComponentSetValue2(character_data, "mVelocity", vx, vy)
end
local velocity_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
if velocity_data ~= nil then
ComponentSetValue2(velocity_data, "mVelocity", vx, vy)
end
2024-05-08 20:33:41 +03:00
EntitySetTransform(enemy_id, x, y)
end
2024-05-12 18:05:25 +03:00
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp-hp
if dmg > 0 then
-- Make sure the enemy doesn't die from the next EntityInflictDamage.
util.set_ent_health(enemy_id, {dmg*2, dmg*2})
-- Deal damage, so that game displays damage numbers.
EntityInflictDamage(enemy_id, dmg, "DAMAGE_CURSE", "", "NONE", 0, 0, GameGetWorldStateEntity())
2024-05-12 18:05:25 +03:00
end
2024-05-08 20:33:41 +03:00
util.set_ent_health(enemy_id, {hp, max_hp})
::continue::
2024-05-08 20:33:41 +03:00
end
end
function enemy_sync.on_projectile_fired(shooter_id, projectile_id, initial_rng, position_x, position_y, target_x, target_y, send_message,
unknown1, multicast_index, unknown3)
local not_a_player = not EntityHasTag(shooter_id, "ew_no_enemy_sync") and not EntityHasTag(shooter_id, "player_unit") and not EntityHasTag(shooter_id, "ew_client")
if not_a_player and ctx.is_host then
local projectileComponent = EntityGetFirstComponentIncludingDisabled(projectile_id, "ProjectileComponent")
local entity_that_shot = ComponentGetValue2(projectileComponent, "mEntityThatShot")
if entity_that_shot == 0 then
rpc.replicate_projectile(np.SerializeEntity(projectile_id), position_x, position_y, target_x, target_y, shooter_id, initial_rng)
end
end
end
rpc.opts_reliable()
function rpc.replicate_projectile(seri_ent, position_x, position_y, target_x, target_y, remote_source_ent, rng)
2024-06-18 14:25:19 +03:00
if rng ~= nil then
np.SetProjectileSpreadRNG(rng)
end
if ctx.entity_by_remote_id[remote_source_ent] == nil then
return
end
local source_ent = ctx.entity_by_remote_id[remote_source_ent].id
local ent = EntityCreateNew()
np.DeserializeEntity(ent, seri_ent)
GameShootProjectile(source_ent, position_x, position_y, target_x, target_y, ent)
end
2024-07-23 14:43:56 +03:00
return enemy_sync