Added New Mods and Profiles Folders

This is a complete rebuild of the modpack, with all new mods and updates for 1.5.3 of Anomaly.
This commit is contained in:
2025-01-14 05:07:53 -05:00
parent 85c665b107
commit 376b4b9689
21217 changed files with 546254 additions and 0 deletions
@@ -0,0 +1,11 @@
-- Custom Callbacks
-- Called when furniture is placed by the player
AddScriptCallback("hf_on_furniture_place") -- Params: (number: id of object)
-- Called when furniture is spawned
-- Occurs before `hf_on_furniture_place`
AddScriptCallback("hf_on_furniture_spawn") -- Params: (number: id of object)
-- Called when furniture is removed (right now, when it is picked up)
-- Must be called when some other script is releasing the furniture object
AddScriptCallback("hf_on_before_furniture_release") -- Params: (number: id of object)
@@ -0,0 +1,132 @@
local function pr(...)
local debug = false
if debug then
printf("aol_bbox_collision: " .. ...)
end
end
-- helper functions
local function diagonal_length(a,b)
return math.sqrt((a*a) + (b*b))
end
-- used to generate coordinates in local space of bounding box from dimensions
-- each index corresponds to a vertex
-- local space origin is at center of box
local box_local_matrix = {
vector():set(-0.5, -0.5, -0.5), -- bottom, top right
vector():set(0.5, -0.5, -0.5), -- bottom, top left
vector():set(-0.5, -0.5, 0.5), -- bottom, bottom right
vector():set(0.5, -0.5, 0.5), -- bottom, bottom left
vector():set(-0.5, 0.5, -0.5), -- top, top right
vector():set(0.5, 0.5, -0.5), -- top, top left
vector():set(-0.5, 0.5, 0.5), -- top, bottom right
vector():set(0.5, 0.5, 0.5), -- top, bottom left
}
local ray_pairs = {
-- Orthogonals
-- bottom plane
{1,2},
{1,3},
{2,4},
{3,4},
-- top plane
{5,6},
{5,7},
{6,8},
{7,8},
-- upward edges
{1,5},
{3,7},
{4,8},
{2,6},
-- Diagonals
-- bottom plane
{1,4},
{2,3},
-- top plane
{6,7},
{5,8},
-- front plane
{3,8},
{4,7},
-- back plane
{1,6},
{2,5},
-- left plane
{2,8},
{4,6},
-- right plane
{1,7},
{3,5}
}
-- Class for handling collisions with a bounding box, defined by its dimensions and origin
class "bbox_collider" (aol_bshape.bshape_collider)
function bbox_collider:__init(width, length, height, world_origin, local_origin, rotation)
super(world_origin, local_origin, rotation)
-- Stores dimensions of the bounding box
self.bbox_dims = {
width = width,
length = length,
height = height,
}
self.ray_pairs = ray_pairs
self.ray_pair_i2length = {
-- Orthogonals
-- bottom plane
[1] = self.bbox_dims.width,
[2] = self.bbox_dims.length,
[3] = self.bbox_dims.length,
[4] = self.bbox_dims.width,
-- top plane
[5] = self.bbox_dims.width,
[6] = self.bbox_dims.length,
[7] = self.bbox_dims.length,
[8] = self.bbox_dims.width,
-- upward edges
[9] = self.bbox_dims.height,
[10] = self.bbox_dims.height,
[11] = self.bbox_dims.height,
[12] = self.bbox_dims.height,
-- Diagonals
-- bottom plane
[13] = diagonal_length(self.bbox_dims.width,self.bbox_dims.length),
[14] = diagonal_length(self.bbox_dims.width,self.bbox_dims.length),
-- top plane
[15] = diagonal_length(self.bbox_dims.width,self.bbox_dims.length),
[16] = diagonal_length(self.bbox_dims.width,self.bbox_dims.length),
-- front plane
[17] = diagonal_length(self.bbox_dims.width,self.bbox_dims.height),
[18] = diagonal_length(self.bbox_dims.width,self.bbox_dims.height),
-- back plane
[19] = diagonal_length(self.bbox_dims.width,self.bbox_dims.height),
[20] = diagonal_length(self.bbox_dims.width,self.bbox_dims.height),
-- left plane
[21] = diagonal_length(self.bbox_dims.length,self.bbox_dims.height),
[22] = diagonal_length(self.bbox_dims.length,self.bbox_dims.height),
-- right plane
[23] = diagonal_length(self.bbox_dims.length,self.bbox_dims.height),
[24] = diagonal_length(self.bbox_dims.length,self.bbox_dims.height)
}
self:UpdateVertices()
self:UpdateBBox()
end
function bbox_collider:UpdateVertices()
for i, pos in ipairs(box_local_matrix) do
local vertex_pos = vec_add(pos, self.local_origin)
vertex_pos.x = vertex_pos.x*self.bbox_dims.width
vertex_pos.y = vertex_pos.y*self.bbox_dims.height
vertex_pos.z = vertex_pos.z*self.bbox_dims.length
self.vertices[i] = vertex_pos
end
end
@@ -0,0 +1,375 @@
last_gizmo_id = 696969
color_colliding = fcolor():set(1,0,0,1)
color_not_colliding = fcolor():set(0,1,0,1)
color_not_colliding_front = fcolor():set(0.25,1,0.75,1)
color_contact = fcolor():set(1,1,0,1)
color_side = fcolor():set(0.5,0.5,0.5,1)
-- Generic Class for handling collisions with a bounding shape
-- supposed to be subclassed into a useful shape (see aol_bbox)
class "bshape_collider"
function bshape_collider:__init(world_origin, local_origin, rotation)
-- Coordinates of the origin in local space (relative to self.vertices)
self.local_origin = local_origin or vector():set(0,0,0)
-- Coordinates of the entire box's origin in world space (relative to game map)
self.world_origin = world_origin or vector():set(0,0,0)
self.rotation = rotation
self.is_colliding = false
-- Coordinates of bbox's vertices in local space
self.vertices = {}
-- Coordinates of bbox vertices in world space
self.world_vertices = {}
-- Pairs of vertices to perform raycasting between
self.ray_pairs = {}
-- Map of ray pair index to it's length
self.ray_pair_i2length = {}
end
function bshape_collider:UpdateBBox()
-- Get position of vertices in bounding box in world space
for i, local_vertex in ipairs(self.vertices) do
-- local space
local position = vector():set(local_vertex.x, local_vertex.y, local_vertex.z)
-- rotate with quaternion
if self.rotation then
position = self.rotation:rotate_vector(position)
end
-- world space
position = position:add(self.world_origin)
-- store vertices coordinates in world space of bbox
self.world_vertices[i] = position
end
end
function bshape_collider:UpdateLengths()
for i, ray_pair in ipairs(self.ray_pairs) do
local v1 = self.world_vertices[ray_pair[1]]
local v2 = self.world_vertices[ray_pair[2]]
local length = v1:distance_to(v2)
self.ray_pair_i2length[i] = length
end
end
function bshape_collider:CheckForCollisions()
-- Check for collisions by ray casting between vertex pairs
-- Pairs are specified in ray_pairs
-- Ray cast in both directions in case the ray goes into the world (rays only register collisions when hitting onto a normal plane)
local collision_found = false
for i, ray_pair in ipairs(self.ray_pairs) do
-- Exit early if there is no collision
if not collision_found then
local ray_length = self.ray_pair_i2length[i]
local ray = demonized_geometry_ray.geometry_ray({
ray_range=4,
contact_range=ray_length,
flags=(1+2)})
local ray_direction = vec_sub(self.world_vertices[ray_pair[1]], self.world_vertices[ray_pair[2]])
ray_direction = ray_direction:normalize()
-- Perform raycasts
local ray_result_rev = ray:get(vec_set(self.world_vertices[ray_pair[1]]), vec_set(ray_direction):invert())
local ray_result = ray:get(vec_set(self.world_vertices[ray_pair[2]]), vec_set(ray_direction))
if ray_result.in_contact or ray_result_rev.in_contact then
collision_found = true
end
end
end
self.is_colliding = collision_found
return collision_found
end
-- Setters
function bshape_collider:SetLocalOrigin(new_pos)
self.local_origin = vec_set(new_pos)
-- update vertices of bbox in local space
self:UpdateVertices()
self:UpdateBBox()
end
function bshape_collider:SetWorldOrigin(new_pos)
self.world_origin = vec_set(new_pos)
end
-- Functions to be completed by subclasses
function bshape_collider:UpdateVertices()
end
-- Class for rendering bounding boxes with particles
class "bshape_renderer"
function bshape_renderer:__init()
self.particles = {}
self.gizmos = nil
self.contact_gizmos = {}
self.side_gizmos = nil
--ray for testing distance to walls and drawing distance lines
self.ray = demonized_geometry_ray.geometry_ray({ ray_range=1, contact_range=0.5, flags=(1+2)})
end
function bshape_renderer:DrawBShapeCollider(bshape_collider)
if #bshape_collider.world_vertices >= 8 then
self:DrawCube(bshape_collider)
if bshape_collider.is_colliding == false then
self:CheckCubeCollision(bshape_collider)
else
self:StopContactGizmos()
self:StopSideGizmos()
end
else
self:DrawLegacy(bshape_collider)
end
end
function bshape_renderer:DrawCube(bshape_collider)
if self.gizmos == nil then
self.gizmos = {
self:AddCubeLine(bshape_collider, 1, 2, true, false),
self:AddCubeLine(bshape_collider, 2, 4, false, false),
self:AddCubeLine(bshape_collider, 3, 4, false, false),
self:AddCubeLine(bshape_collider, 1, 3, false, false),
self:AddCubeLine(bshape_collider, 5, 6, true, false),
self:AddCubeLine(bshape_collider, 6, 8, false, false),
self:AddCubeLine(bshape_collider, 7, 8, false, false),
self:AddCubeLine(bshape_collider, 5, 7, false, false),
self:AddCubeLine(bshape_collider, 1, 5, true, false),
self:AddCubeLine(bshape_collider, 2, 6, true, false),
self:AddCubeLine(bshape_collider, 3, 7, false, false),
self:AddCubeLine(bshape_collider, 4, 8, false, false)
}
end
if self.side_gizmos == nil then
self.side_gizmos = {
["front"] = {
self:AddCubeLine(bshape_collider, 1, 6, false, true),
self:AddCubeLine(bshape_collider, 2, 5, false, true)
},
["back"] = {
self:AddCubeLine(bshape_collider, 3, 8, false, true),
self:AddCubeLine(bshape_collider, 4, 7, false, true)
},
["left"] = {
self:AddCubeLine(bshape_collider, 1, 7, false, true),
self:AddCubeLine(bshape_collider, 3, 5, false, true)
},
["right"] = {
self:AddCubeLine(bshape_collider, 2, 8, false, true),
self:AddCubeLine(bshape_collider, 4, 6, false, true)
},
["top"] = {
self:AddCubeLine(bshape_collider, 5, 8, false, true),
self:AddCubeLine(bshape_collider, 6, 7, false, true)
},
["bottom"] = {
self:AddCubeLine(bshape_collider, 1, 4, false, true),
self:AddCubeLine(bshape_collider, 2, 3, false, true)
}
}
self:StopSideGizmos()
end
for i, gizmo in ipairs(self.gizmos) do
gizmo.line.visible = true
gizmo.line.color = bshape_collider.is_colliding and aol_bshape.color_colliding
or (gizmo.is_front and aol_bshape.color_not_colliding_front or aol_bshape.color_not_colliding)
gizmo.line.point_a = bshape_collider.world_vertices[gizmo.vertex_1]
gizmo.line.point_b = bshape_collider.world_vertices[gizmo.vertex_2]
end
end
function bshape_renderer:CheckCubeCollision(bshape_collider)
if bshape_collider == nil then
return
end
local verts = bshape_collider.world_vertices
local dir_front = vec_sub(verts[3], verts[1]):normalize():invert()
local dir_back = vec_sub(verts[1], verts[3]):normalize():invert()
local dir_left = vec_sub(verts[2], verts[1]):normalize():invert()
local dir_right = vec_sub(verts[1], verts[2]):normalize():invert()
local dir_top = vec_sub(verts[1], verts[5]):normalize():invert()
local dir_bottom = vec_sub(verts[5], verts[1]):normalize():invert()
self:CheckSideCollision(bshape_collider, dir_front, "front", 1, 2, 5, 6, 9)
self:CheckSideCollision(bshape_collider, dir_back, "back", 3, 4, 7, 8, 10)
self:CheckSideCollision(bshape_collider, dir_left, "left", 1, 3, 5, 7, 11)
self:CheckSideCollision(bshape_collider, dir_right, "right", 2, 4, 6, 8, 12)
self:CheckSideCollision(bshape_collider, dir_top, "top", 5, 6, 7, 8, 13)
self:CheckSideCollision(bshape_collider, dir_bottom, "bottom", 1, 2, 3, 4, 14)
end
function bshape_renderer:CheckSideCollision(bshape_collider, direction, direction_name, vertex_1, vertex_2, vertex_3, vertex_4, vertex_5)
local valid = bshape_collider and direction and direction_name and vertex_1 and vertex_2 and vertex_3 and vertex_4 and vertex_5
if not valid then
return
end
local verts = bshape_collider.world_vertices
local center_pos = self:GetSideCenterPosition(bshape_collider, vertex_1, vertex_4)
local result_1 = self:CheckRayCollision(bshape_collider, direction, direction_name, verts[vertex_1], vertex_1)
local result_2 = self:CheckRayCollision(bshape_collider, direction, direction_name, verts[vertex_2], vertex_2)
local result_3 = self:CheckRayCollision(bshape_collider, direction, direction_name, verts[vertex_3], vertex_3)
local result_4 = self:CheckRayCollision(bshape_collider, direction, direction_name, verts[vertex_4], vertex_4)
local result_5 = self:CheckRayCollision(bshape_collider, direction, direction_name, center_pos, vertex_5)
local in_contact = result_1 or result_2 or result_3 or result_4 or result_5
self.side_gizmos[direction_name][1].line.visible = in_contact
self.side_gizmos[direction_name][1].line.point_a = verts[vertex_1]
self.side_gizmos[direction_name][1].line.point_b = verts[vertex_4]
self.side_gizmos[direction_name][2].line.visible = in_contact
self.side_gizmos[direction_name][2].line.point_a = verts[vertex_2]
self.side_gizmos[direction_name][2].line.point_b = verts[vertex_3]
end
function bshape_renderer:GetSideCenterPosition(bshape_collider, vertex_1, vertex_2)
if bshape_collider == nil or vertex_1 == nil or vertex_2 == nil then
return
end
local pos1 = bshape_collider.world_vertices[vertex_1]
local pos2 = bshape_collider.world_vertices[vertex_2]
local add = vector():set(pos1):add(pos2)
local div = vector():set(add):div(vector():set(2,2,2))
return div
end
function bshape_renderer:CheckRayCollision(bshape_collider, direction, direction_name, position, vertex)
local valid = bshape_collider and direction and direction_name and position and vertex
if not valid then
return
end
local gizmo_name = direction_name .. "_" .. vertex
local ray_result = self.ray:get(vec_set(position), vec_set(direction))
if ray_result.distance < 0.5 then
if self.contact_gizmos[gizmo_name] == nil then
self.contact_gizmos[gizmo_name] = self:AddLine(position, ray_result.position, aol_bshape.color_contact)
end
self.contact_gizmos[gizmo_name].visible = true
self.contact_gizmos[gizmo_name].point_a = position
self.contact_gizmos[gizmo_name].point_b = ray_result.position
return true
else
if self.contact_gizmos[gizmo_name] ~= nil then
self.contact_gizmos[gizmo_name].visible = false
end
return false
end
end
function bshape_renderer:DrawLegacy(bshape_collider)
for i, position in ipairs(bshape_collider.world_vertices) do
if self.particles[i] == nil then
self.particles[i] = particles_object("_samples_particles_\\place_indicator")
end
local particle = self.particles[i]
if not particle:playing() then
particle:play()
end
-- Move particles to origin if bbox collides with something
if bshape_collider.is_colliding then
particle:move_to(bshape_collider.world_origin, VEC_Z)
-- Move particles to vertices of bbox if there is no collision
else
particle:move_to(position, VEC_Z)
end
end
end
function bshape_renderer:AddCubeLine(bshape_collider, vertex_1, vertex_2, is_front, is_side)
if bshape_collider == nil or vertex_1 == nil or vertex_2 == nil or is_front == nil or is_side == nil then
return
end
local pos1 = bshape_collider.world_vertices[vertex_1]
local pos2 = bshape_collider.world_vertices[vertex_2]
local color = aol_bshape.color_not_colliding
if is_front then
color = aol_bshape.color_not_colliding_front
elseif is_side then
color = aol_bshape.color_side
end
local line = self:AddLine(pos1, pos2, color)
return {
line = line,
vertex_1 = vertex_1,
vertex_2 = vertex_2,
is_front = is_front
}
end
function bshape_renderer:AddLine(position_1, position_2, color)
if position_1 == nil or position_2 == nil or color == nil then
return
end
aol_bshape.last_gizmo_id = aol_bshape.last_gizmo_id + 1
local line = debug_render.add_object(aol_bshape.last_gizmo_id, DBG_ScriptObject.line):cast_dbg_line()
line.visible = true
line.color = color
line.point_a = position_1
line.point_b = position_2
return line
end
function bshape_renderer:Stop()
self:StopParticles()
self:StopGizmos()
self:StopContactGizmos()
self:StopSideGizmos()
end
function bshape_renderer:StopParticles()
for i, particle in ipairs(self.particles) do
if particle then
particle:stop_deffered()
end
end
end
function bshape_renderer:StopGizmos()
for i, gizmo in ipairs(self.gizmos) do
gizmo.line.visible = false
end
end
function bshape_renderer:StopContactGizmos()
for key, gizmo in pairs(self.contact_gizmos) do
gizmo.visible = false
end
end
function bshape_renderer:StopSideGizmos()
for side_name, side in pairs(self.side_gizmos) do
for i, gizmo in ipairs(side) do
gizmo.line.visible = false
end
end
end
-- MCM Options
@@ -0,0 +1,231 @@
class "Quaternion"
function Quaternion:__init(s, v)
if type(s) == "userdata" and
s.x and s.y and s.z then
-- XRay's order of rotations is ZXY
local unit_z = vector():set(0,0,1)
local q_z = get_rotation_around(unit_z, s.z)
-- X
local unit_x = vector():set(1,0,0)
local q_x = get_rotation_around(unit_x, s.x)
-- Y
local unit_y = vector():set(0,1,0)
local q_y = get_rotation_around(unit_y, s.y)
local q_rot = q_z:multiply(q_x):multiply(q_y)
self.w = q_rot.w
self.x = q_rot.x
self.y = q_rot.y
self.z = q_rot.z
else
self.w = s or 1
self.x = v and v.x or 0
self.y = v and v.y or 0
self.z = v and v.z or 0
end
end
function Quaternion:normalize()
local w2 = self.w*self.w
local x2 = self.x*self.x
local y2 = self.y*self.y
local z2 = self.z*self.z
local length = math.sqrt(w2+x2+y2+z2)
self.w = self.w/length
self.x = self.x/length
self.y = self.y/length
self.z = self.z/length
end
-- Using Laurent Couvidou's optimal code:
-- https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion
function Quaternion:rotate_vector(v)
-- Z and Y are flipped because XRay flips them around, so this is set to the
-- standard XYZ notation for calculation before being flipped back to XZY
local v = vector():set(v.x, v.z, v.y)
local u = vector():set(self.x, self.z, self.y)
local s = self.w
local v0 = vector():set(u):mul(vector():set(u):dotproduct(v)):mul(2.0)
local v1 = vector():set(v):mul((s*s) - vector():set(u):dotproduct(u))
local v2 = vector():set(0, 0, 0):crossproduct(u, v):mul(2*s)
local new_v = vector():set(v0):add(v1):add(v2)
return vector():set(new_v.x, new_v.z, new_v.y)
end
function Quaternion:multiply(q)
local q_v = vector():set(q.x, q.y, q.z)
local self_v = vector():set(self.x, self.y, self.z)
local w = (self.w*q.w) - (vector():set(self_v):dotproduct(vector():set(q_v)))
local v0 = vector():set(0, 0, 0):crossproduct(self_v, q_v)
local v1 = vector():set(q_v):mul(self.w)
local v2 = vector():set(self_v):mul(q.w)
local v = vector():set(v0):add(v1):add(v2)
return this.Quaternion(w, v)
end
-- Shamelessly taken from three.js
function Quaternion:to_euler_angles()
local angles = vector():set(0, 0, 0)
local x = self.x
local y = self.y
local z = self.z
local w = self.w
local x2 = x + x
local y2 = y + y
local z2 = z + z
local xx = x * x2
local xy = x * y2
local xz = x * z2
local yy = y * y2
local yz = y * z2
local zz = z * z2
local wx = w * x2
local wy = w * y2
local wz = w * z2
local te = {}
te[0] = ( 1 - ( yy + zz ) )
te[1] = ( xy + wz )
te[2] = ( xz - wy )
te[4] = ( xy - wz )
te[5] = ( 1 - ( xx + zz ) )
te[6] = ( yz + wx )
te[8] = ( xz + wy )
te[9] = ( yz - wx )
te[10] = ( 1 - ( xx + yy ) )
local m11 = te[0]
local m12 = te[4]
local m13 = te[8];
local m21 = te[1]
local m22 = te[5]
local m23 = te[9];
local m31 = te[2]
local m32 = te[6]
local m33 = te[10];
-- ZXY
angles.x = math.asin( clamp( m32, - 1, 1 ) );
if ( math.abs( m32 ) < 0.9999999 ) then
angles.y = math.atan2( - m31, m33 );
angles.z = math.atan2( - m12, m22 );
else
angles.y = 0
angles.z = math.atan2( m21, m11 );
end
-- XZY
-- angles.z = math.asin( - clamp( m12, - 1, 1 ) );
-- if ( math.abs( m12 ) < 0.9999999 ) then
-- angles.x = math.atan2( m32, m22 );
-- angles.y = math.atan2( m13, m11 );
-- else
-- angles.x = math.atan2( -m23, m33 );
-- angles.y = 0
-- end
return angles
end
function Quaternion:to_string()
return("[w:" .. self.w .. ", x:" .. self.x .. ", y:" .. self.y .. ", z:" .. self.z .. "]")
end
----------
function length_2(u)
local length = u:magnitude()
return length*length
end
function orthogonal(v)
local x = math.abs(v.x)
local y = math.abs(v.y)
local z = math.abs(v.z)
local other = nil
if x < y then
if x < z then
other = vector():set(1, 0, 0)
else
other = vector():set(0, 0, 1)
end
else
if y < z then
other = vector():set(0, 1, 0)
else
other = vector():set(0, 0, 1)
end
end
return vector():set(0, 0, 0):crossproduct(v, other);
end
function get_rotation_between(u, v)
local u = vector():set(u.x, u.z, u.y)
local v = vector():set(v.x, v.z, v.y)
local k_cos_theta = vector():set(u):dotproduct(v);
local k = math.sqrt(length_2(u) * length_2(v));
if (k_cos_theta / k == -1) then
-- if (k_cos_theta > 0.999999 or k_cos_theta < -0.999999) then
-- printf("ortho? " .. k_cos_theta)
-- 180 degree rotation around any orthogonal vector
local u_ortho = orthogonal(u):normalize()
return this.Quaternion(0, u_ortho);
end
local q = this.Quaternion(k_cos_theta + k, vector():set(0, 0, 0):crossproduct(u, v))
q:normalize()
local old_y = q.y
local old_z = q.z
q.y = old_z
q.z = old_y
return q
end
function get_rotation_around(v, angle)
local q = this.Quaternion()
q.w = math.cos(angle/2)
q.x = v.x * math.sin(angle/2)
q.y = v.y * math.sin(angle/2)
q.z = v.z * math.sin(angle/2)
q:normalize()
return q
end
function rotate_vector_by_euler(v_loc, v_rot)
local q_rot = Quaternion(v_rot)
return q_rot:rotate_vector(v_loc)
end
@@ -0,0 +1,462 @@
---------------------
-- Helper functions
---------------------
function move_and_center_element(element, x, y)
local pos = vector2()
pos.x = x - element:GetWidth()/2
pos.y = y - element:GetHeight()/2
element:SetWndPos(pos)
end
---Scales UI elements to correct ratio after UI scaling
---Written by RavenAscendant, Yoinked and Commented by Aoldri
---@param ele CUIStatic
---@param adjust_x? boolean
---@param anchor? boolean
---@param anchor_point? `left`|`right`|`center`
---@param parent_width? number
function scale_ui(ele, adjust_x, anchor, anchor_point, parent_width)
p_width = parent_width or 1024
p_center = p_width/2
width = ele:GetWidth()
pos = ele:GetWndPos()
anchorpos = {}
anchorpos.left = pos.x
anchorpos.right = anchorpos.left + width
anchorpos.center = anchorpos.left + width/2
ratio = (device().height / device().width) / (768 / 1024)
xadjust = anchorpos.left
if adjust_x then
if anchor_point == "right" then
xadjust = p_width - (p_width - (anchor and anchorpos[anchor] or anchorpos.left))*ratio
elseif anchor_point == "center" then
xadjust = p_center - (p_center - (anchor and anchorpos[anchor] or anchorpos.left))*ratio
else
xadjust = ratio * (anchor and anchorpos[anchor] or anchorpos.left)
end
end
ele:SetWndSize(vector2():set(ele:GetWidth() * ratio, ele:GetHeight()))
ele:SetWndPos( vector2():set(xadjust , pos.y ) )
end
-----------
-- States
-----------
----Enum for option states
---@enum States
States = {
ENABLED = 1, -- Option is visible and accessible
DISABLED = 2, -- Option is visible, but not accessible
HIGHLIGHTED = 3, -- Option is 'selected' or 'active'
TOUCHED = 4, -- Mouse is hovering over option
}
-- Useful functions
State2Suffix = {
[States.ENABLED] = "_e",
[States.DISABLED] = "_d",
[States.HIGHLIGHTED] = "_h",
[States.TOUCHED] = "_t",
}
---Appends a texture name with a suffix corresponding to the state, like textures for CUI3tButton
---@param texture string
---@param state States
---@param fallback? string
---@return string
function get_stateful_texture(texture, state, fallback)
local suffix = State2Suffix[state]
if suffix then return texture .. suffix end
return fallback
end
State2Colour = {
[States.ENABLED] = GetARGB(70, 255, 255, 255),
[States.DISABLED] = GetARGB(70, 255, 255, 255),
[States.HIGHLIGHTED] = GetARGB(255, 255, 255, 255),
[States.TOUCHED] = GetARGB(255, 255, 255, 255),
}
---Gets a colour dependent on option state
---@param state States
---@return number
function get_stateful_colour(state)
return State2Colour[state]
end
------------------------------------------------------------------
-- UI
-------------------------------------------------------------------
class "OptionData"
---@param callback_id string
---@param texture string|fun(States): string
function OptionData:__init(callback_id, texture)
self.callback_id = callback_id
self:SetTexture(texture)
self.state = States.ENABLED
end
-- Texture
---@param texture string|fun(States): string
function OptionData:SetTexture(texture)
self.texture = texture
end
---@return string
function OptionData:GetTexture()
return self:GetAttribute("texture")
end
-- Text
---@param text {title: string, description: string}
function OptionData:SetText(text)
if not text.description then text.description = "" end
self.text = text
end
---@return {title: string, description: string}
function OptionData:GetText()
return self:GetAttribute("text") or {title="", description=""}
end
-- Colour
---@param colour number|fun(States): number
function OptionData:SetColour(colour)
self.colour = colour
end
---@return number
function OptionData:GetColour()
return self:GetAttribute("colour") or GetARGB(255, 255, 255, 255)
end
-- State
---@param state States
function OptionData:SetState(state)
self.state = state
end
---@return States
function OptionData:GetState()
return self.state
end
---@param state States
---@return boolean
function OptionData:IsState(state)
return self.state == state
end
---@param attribute string
---@return any
function OptionData:GetAttribute(attribute)
local value = nil
if type(self[attribute]) == "function" then
value = self[attribute](self.state)
else
value = self[attribute]
end
return value
end
---
class "UIRadialMenu" (CUIScriptWnd)
function UIRadialMenu:__init() super()
self:SetWndRect(Frect():set(0,0,1024,768))
self:AllowMovement(true)
self:SetAutoDelete(true)
self.xml = CScriptXmlInit()
self.xml:ParseFile("ui_radial_menu.xml")
self.callbacks = {}
---@type table<number, arm.OptionData>
self.option_data = {} -- Holds info about each option
---@type table<number, {icon: CUIStatic, hover_bg: CUIStatic, selected_bg: CUIStatic, angle: number}>
self.options = {} -- Holds UI elements for each option
-- Calculate center of screen
self.center = vector2():set(self:GetWidth()/2, self:GetHeight()/2)
-- Background
self.bg = self.xml:InitStatic("bg", self)
move_and_center_element(self.bg, self.center.x, self.center.y)
scale_ui(self.bg, true, true, "center")
-- Cursor
self.cursor = self.xml:InitStatic("cursor", self)
self.cursor:EnableHeading(true)
move_and_center_element(self.cursor, self.center.x, self.center.y-self.cursor:GetHeight()/2)
-- not sure why i don't have to re-scale this element
-- scale_ui(self.cursor, true, true, "center")
-- Central label
self.title = self.xml:InitTextWnd("title", self)
move_and_center_element(self.title, self.title:GetWndPos().x, self.title:GetWndPos().y)
-- Central description
self.description = self.xml:InitTextWnd("description", self)
move_and_center_element(self.description, self.description:GetWndPos().x, self.description:GetWndPos().y)
self.hovered_option_i = 1
self:DrawOptions()
end
function UIRadialMenu:__finalize()
end
---- Adds an option to the menu with specific callback id, texture, colour, and initial state.
---- Both `get_texture()` and `get_colour()` are functions with the option state passed as a parameter
---- Options are shown in order of when they are added, starting from the top and rotating clockwise
---@param option_data arm.OptionData
---@return boolean success
function UIRadialMenu:AddOption(option_data)
if not option_data then return false end
if not option_data:GetTexture() then return false end
table.insert(self.option_data, option_data)
return true
end
---Gets an option
---@param callback_id string
---@return arm.OptionData
function UIRadialMenu:GetOption(callback_id)
-- Look through each option and check for callback_id
for i, opt in ipairs(self.option_data) do
if opt.callback_id == callback_id then
return opt
end
end
-- returns nil if invalid callback_id
end
function UIRadialMenu:DrawOptions()
-- Remove old options in case new options were added or options were removed
self:ResetUI()
-- Create buttons
for i, btn_datum in ipairs(self.option_data) do
self.options[i] = {}
-- Create BGs
local hover_bg = self.xml:InitStatic("hover_bg", self)
local selected_bg = self.xml:InitStatic("selected_bg", self)
-- Create Icon
local icon = self.xml:InitStatic("icon", self)
icon:SetStretchTexture(true)
-- Calculate position (relative to centre)
local x = icon:GetWndPos().x
local y = icon:GetWndPos().y
local angle = (2*math.pi * ((i-1)/#self.option_data))
angle = angle --+ 2*math.pi*(1/12)
local new_x = x*math.cos(angle) - y*math.sin(angle)
local new_y = y*math.cos(angle) + x*math.sin(angle)
-- Offset position so that it centers around the middle
new_x = new_x + self.center.x
new_y = new_y + self.center.y
-- printf("new_x: %s, new_y: %s", new_x, new_y)
-- Move to point and center on it
move_and_center_element(icon, new_x, new_y)
scale_ui(icon, true, true, "center")
move_and_center_element(hover_bg, new_x, new_y)
scale_ui(hover_bg, true, true, "center")
hover_bg:Show(false)
move_and_center_element(selected_bg, new_x, new_y)
scale_ui(selected_bg, true, true, "center")
selected_bg:Show(false)
self.options[i].icon = icon
self.options[i].hover_bg = hover_bg
self.options[i].selected_bg = selected_bg
self.options[i].angle = angle
-- Update Icon
self:UpdateIcon(i)
end
end
function UIRadialMenu:OnButtonClick(i)
if self.option_data[i]:IsState(States.DISABLED) then return end
---@alias callback_flags {close_gui: boolean}
local flags = {
close_gui = true,
}
self:SendCallback(self.option_data[i].callback_id, flags)
if flags.close_gui then
self:Close()
end
end
-- TODO: Rewrite logic to look better maybe
function UIRadialMenu:OnButtonHover(i)
if self.hovered_option_i == i then return end
-- New option
self.options[i].hover_bg:Show(true)
self.title:SetText(self.option_data[i]:GetText().title)
self.title:AdjustHeightToText()
self.description:SetText(self.option_data[i]:GetText().description)
self.description:SetWndPos(vector2():set(self.description:GetWndPos().x, self.title:GetWndPos().y + self.title:GetHeight() + 8))
if self.option_data[i]:IsState(States.ENABLED) then
self.option_data[i]:SetState(States.TOUCHED)
end
-- Previous option
self.options[self.hovered_option_i].hover_bg:Show(false)
if self.option_data[self.hovered_option_i]:IsState(States.TOUCHED) then
self.option_data[self.hovered_option_i]:SetState(States.ENABLED)
end
self.hovered_option_i = i
end
function UIRadialMenu:SendCallback(callback_id, ...)
local funcs = self.callbacks[callback_id]
if not funcs then return end
for i, func in ipairs(funcs) do
func(...)
end
end
---Register a function to be called when an option with the corresponding callback_id is selected.
---Each function is executed in order of when they were registered with this function.
---
---This function should take in one parameter: `flags`
---
--- - flags.close_gui will close the menu if `true` (default: `true`)
---@param callback_id string
---@param callback_function fun(flags: callback_flags)
function UIRadialMenu:RegisterCallback(callback_id, callback_function)
self.callbacks[callback_id] = self.callbacks[callback_id] or {}
table.insert(self.callbacks[callback_id], callback_function)
end
function UIRadialMenu:ResetUI()
-- Remove pre-existing options
if self.options ~= nil then
for i, opt in ipairs(self.options) do
self:DetachChild(opt.icon)
self:DetachChild(opt.hover_bg)
self:DetachChild(opt.selected_bg)
end
end
self.options = {}
end
function UIRadialMenu:Reset()
self:ResetUI()
self.option_data = {}
self.callbacks = {}
end
function UIRadialMenu:UpdateIcon(i)
local icon = self.options[i].icon
local data = self.option_data[i]
-- Set Texture
icon:InitTexture(data:GetTexture())
-- Set Colour
icon:SetTextureColor(data:GetColour())
if data:IsState(States.HIGHLIGHTED) then
self.options[i].selected_bg:Show(true)
else
self.options[i].selected_bg:Show(false)
end
end
function UIRadialMenu:Update()
CUIScriptWnd.Update(self)
-- Update Cursor pos and rot
local cur_pos = vector2():set(0, -self.cursor:GetHeight()/2)
local mouse_pos = GetCursorPosition()
local x_scale = (device().height / device().width) / (768 / 1024)
-- Get angle from center of screen to mouse cursor
-- Account for scaling from 1024x768 so that the cursor rotates towards cursor properly
local angle = math.atan2(-(mouse_pos.x-self.center.x)/x_scale, -(mouse_pos.y-self.center.y))
angle = (angle < 0) and 2*math.pi+angle or angle
angle = 2*math.pi - angle
local new_x = cur_pos.x*math.cos(angle) - cur_pos.y*math.sin(angle)
local new_y = cur_pos.y*math.cos(angle) + cur_pos.x*math.sin(angle)
-- Scale Correction
new_x = new_x*x_scale
-- Move to orbit around center of screen
new_x = new_x + self.center.x
new_y = new_y + self.center.y
move_and_center_element(self.cursor, new_x, new_y)
self.cursor:SetHeading(2*math.pi - angle)
-- Get option from angle
local best_i = nil
local best_similarity = nil
for i, option in ipairs(self.options) do
-- Ignore disabled options
-- if not self.option_data[i]:IsState(States.DISABLED) then
local similarity = math.pi - math.abs(math.fmod(math.abs(angle - option.angle), 2*math.pi) - math.pi)
if best_similarity == nil or similarity < best_similarity then
best_similarity = similarity
best_i = i
end
-- end
end
if best_i then self:OnButtonHover(best_i) end
-- Update option textures
for i, option in ipairs(self.options) do
self:UpdateIcon(i)
end
end
function UIRadialMenu:OnAccept()
end
function UIRadialMenu:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if (res == false) then
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
self:Close()
end
if dik == DIK_keys.MOUSE_1 then
self:OnButtonClick(self.hovered_option_i)
end
end
end
return res
end
function UIRadialMenu:Close()
self:HideDialog()
Unregister_UI("UIRadialMenu")
end
@@ -0,0 +1,208 @@
gc = game.translate_string
---@param id number
---@return bind_hf_base.hf_binder_wrapper|nil
function get_wrapper(id)
local obj = get_object_by_id(id)
if not obj then return end
local binder = obj:binded_object()
if not binder then return end
return binder.wrapper
end
function freeze_obj(id)
local obj = level.object_by_id(id)
if not obj then return end
local phys = obj:get_physics_shell()
if not phys then return end
phys:freeze()
local wrapper = get_wrapper(id)
if wrapper then wrapper.is_frozen = true end
return true
end
function init(obj)
obj:bind_object(hf_binder_wrapper(obj).binder)
end
---@param obj game_object
---@param field string
---@return function
function get_function_from_field(obj, field)
local func_string = ini_sys:r_string_ex(obj:section(), field)
if not func_string then return end
local func_split = {}
for s in string.gmatch(func_string, "([^.]+)") do
table.insert(func_split, s)
end
return _G[func_split[1]][func_split[2]]
end
--------------------------------------------------------------------------------
-- Class "hf_binder_wrapper"
--------------------------------------------------------------------------------
class "hf_binder_wrapper"
-- Class constructor
function hf_binder_wrapper:__init(obj)
self.binder = base_binder(obj)
self.binder.wrapper = self
---@type game_object
self.object = obj
-- *hopefully* source of truth of frozenness since objects are not frozen by default
self.is_frozen = false
end
function hf_binder_wrapper:update(delta)
end
function hf_binder_wrapper:net_spawn(se_abstract)
CreateTimeEvent("hf_freeze", self.object:id(), 0, freeze_obj, self.object:id())
return true
end
function hf_binder_wrapper:net_destroy()
-- Remove time event just in case object is released before freezing
RemoveTimeEvent("hf_freeze", self.object:id())
-- Clean up references
self.binder.wrapper = nil
self.binder = nil
self.object = nil
end
function hf_binder_wrapper:use_callback()
local callback = get_function_from_field(self.object, "ui_on_interaction")
if callback then callback(self.object:id()) end
end
function hf_binder_wrapper:use_callback_simple()
local callback = get_function_from_field(self.object, "ui_on_simple_interaction")
if callback then callback(self.object:id()) end
end
function hf_binder_wrapper:is_pickupable()
local data = hf_obj_manager.get_data(self.object:id())
if data.is_world_obj then
return false
end
return true
end
function hf_binder_wrapper:pickup()
local item_section = ini_sys:r_string_ex(self.object:section(), "item_section")
local condition = self.fuel
alife_create_item(item_section, db.actor, {cond=condition}) -- Maybe transfer all data to new item? Probably depends on item type tho
-- Clean up HF-related data
hf_obj_manager.cleanup_data(self.object:id())
alife_release(self.object)
return true
end
---@param bool boolean
function hf_binder_wrapper:set_frozen(bool)
local data = hf_obj_manager.get_data(self.object:id())
if data.is_world_obj then return end
local phys = self.object:get_physics_shell()
if not phys then return end -- idk how an object wouldn't have a physics shell
if bool then
phys:freeze()
else
phys:unfreeze()
end
self.is_frozen = bool
end
--------------------------------------------------------------------------------
-- Class "base_binder"
--------------------------------------------------------------------------------
class "base_binder" (object_binder)
-- Class constructor
function base_binder:__init(obj) super(obj)
self.object:set_tip_text(gc("st_interact"))
---@type bind_hf_base.hf_binder_wrapper
self.wrapper = nil
end
-- Class update
function base_binder:update(delta)
object_binder.update(self, delta)
-- self.object:set_callback(callback.use_object, self.use_callback, self)
if self.wrapper and self.wrapper.update then
self.wrapper:update(delta)
end
end
-- Reload object
function base_binder:reload(section)
object_binder.reload(self, section)
if self.wrapper and self.wrapper.reload then
self.wrapper:reload(section)
end
end
-- Reinitialize object
function base_binder:reinit()
object_binder.reinit(self)
if self.wrapper and self.wrapper.reinit then
self.wrapper:reinit()
end
end
-- Net spawn
function base_binder:net_spawn(se_abstract)
if not(object_binder.net_spawn(self, se_abstract)) then
return false
end
if self.wrapper and self.wrapper.net_spawn then
local result = self.wrapper:net_spawn(se_abstract)
if result == false then return false end
end
return true
end
-- Net destroy
function base_binder:net_destroy()
self:save_data()
if self.wrapper and self.wrapper.net_destroy then
self.wrapper:net_destroy()
end
object_binder.net_destroy(self)
end
-- Standart function for save
function base_binder:net_save_relevant()
return true
end
-- Saving container
function base_binder:save(stpk)
object_binder.save(self, stpk)
if self.wrapper and self.wrapper.save then
self.wrapper:save(stpk)
end
end
-- Loading container
function base_binder:load(stpk)
object_binder.load(self, stpk)
end
-- Save to mdata by calling hf_obj_manager.update_data
function base_binder:save_data()
if self.wrapper and self.wrapper.save_data then
self.wrapper:save_data()
end
end
@@ -0,0 +1,63 @@
function freeze_obj(id)
local obj = level.object_by_id(id)
if not obj then return end
local phys = obj:get_physics_shell()
if not phys then return end
phys:freeze()
return true
end
function init(obj)
obj:bind_object(hf_static_binder(obj))
end
--------------------------------------------------------------------------------
-- Class "hf_static_binder"
--------------------------------------------------------------------------------
class "hf_static_binder" (object_binder)
-- Class constructor
function hf_static_binder:__init(obj) super(obj)
end
-- Class update
function hf_static_binder:update(delta)
object_binder.update(self, delta)
end
-- Reload object
function hf_static_binder:reload(section)
object_binder.reload(self, section)
end
-- Reinitialize object
function hf_static_binder:reinit()
object_binder.reinit(self)
end
-- Net spawn
function hf_static_binder:net_spawn(se_abstract)
if not(object_binder.net_spawn(self, se_abstract)) then
return false
end
CreateTimeEvent("hf_freeze", self.object:id(), 0, freeze_obj, self.object:id())
return true
end
-- Net destroy
function hf_static_binder:net_destroy()
object_binder.net_destroy(self)
-- Remove time event just in case object is released before freezing
RemoveTimeEvent("hf_freeze", self.object:id())
end
-- Standart function for save
function hf_static_binder:net_save_relevant()
return true
end
-- Saving container
function hf_static_binder:save(stpk)
object_binder.save(self, stpk)
end
-- Loading container
function hf_static_binder:load(stpk)
object_binder.load(self, stpk)
end
@@ -0,0 +1,227 @@
function turn_off_lamp(obj_id)
local obj = get_object_by_id(obj_id)
if not (obj) then
return
end
if not (obj.get_hanging_lamp) then
return
end
if not hf_obj_manager.get_data(obj_id).is_on then
obj:get_hanging_lamp():turn_off()
end
return true
end
local function get_time_elapsed()
return game.get_game_time():diffSec(level.get_start_time())
end
gc = game.translate_string
---------------------------------------------------------------------------------------------------
-- Physic objects binding
----------------------------------------------------------------------------------------------------
local lights = {}
local switch = false
local psi_influence_clsid = {
[clsid.poltergeist_s] = true,
[clsid.controller_s] = true,
}
function need_flicker(lamp_obj)
-- Flicker on Emission Wave or Vortex
if (level_environment.get_light_flicker()) then return true end
-- Flicker if Psi Mutant nearby
local psi_influence = false
local function iterate_func(obj)
if (obj and psi_influence_clsid[obj:clsid()] and obj:alive()) then
psi_influence = true
return true
end
end
level.iterate_nearest(lamp_obj:position(), 15, iterate_func)
if psi_influence then return true end
return switch
end
function init(obj)
obj:bind_object(placeable_light_wrapper(obj).binder)
end
---------------------------------------------------------------------------------------------
class "placeable_light_wrapper" (bind_hf_base.hf_binder_wrapper)
function placeable_light_wrapper:__init(obj) super(obj)
lights[obj:id()] = "light_flicker"
-- Get paths to sound files
local section = self.object:section()
self.humming_snd_path = ini_sys:r_string_ex(section,"snd_on")
self.turn_on_snd_path = ini_sys:r_string_ex(section,"snd_turn_on")
self.turn_off_snd_path = ini_sys:r_string_ex(section,"snd_turn_off")
self.particle_path = ini_sys:r_string_ex(section,"particle")
self.hide_bone_off = ini_sys:r_string_ex(section,"hide_bone_off")
if self.particle_path then
self.particles = particles_object(self.particle_path)
end
self.object:set_tip_text(gc("st_interact"))
self.tg = time_global()
self.time_last = get_time_elapsed() -- used to keep track of time difference in between updates
self.max_duration = ini_sys:r_string_ex(section,"fuel_duration")*3600
-- get data
local data = hf_obj_manager.get_data(obj:id())
self.fuel = data and data.condition or 1
self.infinite_fuel = data and data.is_world_obj
self.last_state = data and data.is_on or false
-- Turn off lamp is light is turned off
CreateTimeEvent("hf_light", "turn_off_"..obj:id(), 0, turn_off_lamp, obj:id())
end
function placeable_light_wrapper:update(delta)
bind_hf_base.hf_binder_wrapper.update(self, delta)
self.object:set_callback(callback.death, placeable_light_wrapper.death_callback, self)
local lamp_obj = self.object:get_hanging_lamp()
local is_flickering = lamp_obj:is_flickering()
local is_on = hf_obj_manager.get_data(self.object:id()).is_on
if (self.fuel <= 0.0 or not is_on) then
if lamp_obj:is_on() then lamp_obj:turn_off() end
else
if not lamp_obj:is_on() then
lamp_obj:turn_on()
end
end
if (self.last_state ~= is_on) then
-- Play humming sound
if (is_on) then
if (self.humming == nil and self.humming_snd_path) then
self.humming = sound_object(self.humming_snd_path)
end
if (self.humming ~= nil and not self.humming:playing()) then
self.humming:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d + sound_object.looped)
self.humming.volume = 0.0
self.humming_max = time_global() + 5000
end
-- 'Turn on' sound
if self.turn_on_snd_path then
sound_object(self.turn_on_snd_path):play_no_feedback(self.object, sound_object.s3d, 0, self.object:position(), math.random(5,8), random_float(0.9, 1.1))
end
-- Show bone when on
if self.hide_bone_off then
self.object:set_bone_visible(self.hide_bone_off, true, true, false)
end
-- Stop humming sound
else
if (self.humming ~= nil and self.humming:playing()) then
self.humming:stop()
end
-- 'Turn off' sound
if self.turn_off_snd_path then
sound_object(self.turn_off_snd_path):play_no_feedback(self.object, sound_object.s3d, 0, self.object:position(), random_float(0.5, 0.9), random_float(0.9, 1.1))
end
-- Hide bone when off
if self.hide_bone_off then
self.object:set_bone_visible(self.hide_bone_off, false, true, false)
end
end
end
self.last_state = is_on
-- Slow Startup for Humming Sound
if (self.humming ~= nil and self.humming:playing() and (self.humming_max > time_global())) then
self.humming.volume = 0.6 - (self.humming_max - time_global()) / 5000.0
end
local needflicker = need_flicker(self.object)
if (needflicker and not is_flickering) then
-- color animator flicker on/off chance on/off base time color animator fps
lamp_obj:set_color_animator(lights[self.object:id()], true, math.random(25,75), random_float(0.75,1.75), math.random(10,20))
elseif (not needflicker and is_flickering) then
lamp_obj:reset_color_animator()
end
if (self.humming ~= nil and self.humming:playing()) then
self.humming:set_position(self.object:position())
end
local time_current = get_time_elapsed()
local time_diff = time_current - self.time_last
if is_on then
-- Create and move particles to bone_light
if self.particle_path then
if not self.particles:playing() then
self.particles:play_at_pos(self.object:bone_position("bone_light"))
end
self.particles:move_to(self.object:bone_position("bone_light"), VEC_Z)
end
-- Deplete light fuel by adjusting condition
if not self.infinite_fuel then
self.fuel = self.fuel - (time_diff / self.max_duration)
end
else
-- Stop particles if turned off
if self.particles and self.particles:playing() then
self.particles:stop()
end
end
self.time_last = time_current
self:save_data()
end
function placeable_light_wrapper:net_destroy()
self:save_data()
if (self.humming) then
self.humming:stop()
self.humming = nil
end
if self.particles and self.particles:playing() then
self.particles:stop()
end
lights[self.object:id()] = nil
end
function placeable_light_wrapper:death_callback(victim, who)
if (self.humming) then
self.humming:stop()
self.humming = nil
end
lights[self.object:id()] = nil
end
function placeable_light_wrapper:save_data()
hf_obj_manager.update_data(self.object:id(), {
condition = self.fuel
})
end
@@ -0,0 +1,30 @@
function init(obj)
obj:bind_object(hf_workshop_wrapper(obj).binder)
end
class "hf_workshop_wrapper" (bind_hf_base.hf_binder_wrapper)
function hf_workshop_wrapper:__init(obj) super(obj)
end
function hf_workshop_wrapper:pickup()
local m_data = alife_storage_manager.get_state()
if m_data.workshop_stashes then
local id = m_data.workshop_stashes[self.object:id()]
if id then
local stash_obj = get_object_by_id(id)
if stash_obj then
stash_obj:iterate_inventory_box( function(temp, obj)
stash_obj:transfer_item(obj, db.actor)
end, stash_obj)
end
m_data.workshop_stashes[self.object:id()] = nil
alife_release_id(id)
end
end
bind_hf_base.hf_binder_wrapper.pickup(self)
end
@@ -0,0 +1,328 @@
local enable_debug = false
local print_tip = function(s, ...)
local f = print_tip or printf
if enable_debug then
return f("Geometry Ray: " .. s, ...)
end
end
-- Check for material (engine edit required)
function lshift(x, by)
return x * 2 ^ by
end
function test(x, mask)
return bit_and(x, mask) == mask
end
local flags_test = {
["flBreakable"] = lshift(1, 0),
["flBounceable"] = lshift(1, 2),
["flSkidmark"] = lshift(1, 3),
["flBloodmark"] = lshift(1, 4),
["flClimable"] = lshift(1, 5),
["flPassable"] = lshift(1, 7),
["flDynamic"] = lshift(1, 8),
["flLiquid"] = lshift(1, 9),
["flSuppressShadows"] = lshift(1, 10),
["flSuppressWallmarks"] = lshift(1, 11),
["flActorObstacle"] = lshift(1, 12),
["flNoRicoshet"] = lshift(1, 13),
["flInjurious"] = lshift(1, 28),
["flShootable"] = lshift(1, 29),
["flTransparent"] = lshift(1, 30),
["flSlowDown"] = lshift(1, 31),
}
--[[
// material exports
.def_readonly( "material_name" , &script_rq_result::pMaterialName )
.def_readonly( "material_flags" , &script_rq_result::pMaterialFlags )
.def_readonly( "material_phfriction" , &script_rq_result::fPHFriction )
.def_readonly( "material_phdamping" , &script_rq_result::fPHDamping )
.def_readonly( "material_phspring" , &script_rq_result::fPHSpring )
.def_readonly( "material_phbounce_start_velocity" , &script_rq_result::fPHBounceStartVelocity )
.def_readonly( "material_phbouncing" , &script_rq_result::fPHBouncing )
.def_readonly( "material_flotation_factor" , &script_rq_result::fFlotationFactor )
.def_readonly( "material_shoot_factor" , &script_rq_result::fShootFactor )
.def_readonly( "material_shoot_factor_mp" , &script_rq_result::fShootFactorMP )
.def_readonly( "material_bounce_damage_factor" , &script_rq_result::fBounceDamageFactor )
.def_readonly( "material_injurious_speed" , &script_rq_result::fInjuriousSpeed )
.def_readonly( "material_vis_transparency_factor" , &script_rq_result::fVisTransparencyFactor )
.def_readonly( "material_snd_occlusion_factor" , &script_rq_result::fSndOcclusionFactor )
.def_readonly( "material_density_factor" , &script_rq_result::fDensityFactor )
]]
-- Geometry Ray class by Thial, edited by demonized
class "geometry_ray"
--[[
(At least one range parameter should be specified)
ray_range:
Defines the total range of the ray. If you want to attach the ray to
a fast moving object it is good to extend the ray so that you can reduce
the polling rate by using the get function.
contact_range:
Defines the distance at which the result will report being in contact.
You can skip it or set it to a value lower than the ray_range to
still be able to get the intersection position from the result while
marking the ray as not being in contact yet
distance_offset:
Defines how much the intersection position is offset.
You can use both positive and negative values or you can leave it blank.
flags (bit map = values can be added together for combined effect):
0 : None
1 : Objects
2 : Statics
4 : Shapes
8 : Obstacles
]]--
function geometry_ray:__init(args)
local args = args or {}
if args.ray_range == nil and args.contact_range == nil then
return nil
end
self.ray_range = args.ray_range or args.contact_range
self.contact_range = args.contact_range or args.ray_range
self.distance_offset = args.distance_offset ~= nil and args.distance_offset or 0
self.ray = ray_pick()
self.ray:set_flags(args.flags or 2)
self.ray:set_range(self.ray_range)
self.visualize = args.visualize
if args.ignore_object then
self.ray:set_ignore_object(args.ignore_object)
end
end
--[[
position:
position from which the ray will start
direction:
direction in which the ray will be fired
]]--
function geometry_ray:get(position, direction)
if position == nil or direction == nil then
return nil
end
local position = vector():set(position)
local direction = vector():set(direction)
self.ray:set_position(position)
self.ray:set_direction(direction)
local res = self.ray:query()
local distance = res and self.ray:get_distance() or self.ray_range
local result = {}
if self.visualize then
local init_pos = vector():set(position)
local end_pos = vector():mad(init_pos, direction, distance)
VisualizeRay(init_pos, end_pos)
end
result.in_contact = distance <= self.contact_range
result.position = position:add(direction:mul(distance + self.distance_offset))
result.distance = distance
result.raw_distance = self.ray:get_distance()
result.success = res
result.object = self.ray:get_object()
result.element = self.ray:get_element()
result.result = self.ray:get_result()
-- Cast to Lua table
-- if result.result then
-- local r = {}
-- r.material_name = result.result.material_name
-- r.material_flags = result.result.material_flags
-- r.material_phfriction = result.result.material_phfriction
-- r.material_phdamping = result.result.material_phdamping
-- r.material_phspring = result.result.material_phspring
-- r.material_phbounce_start_velocity = result.result.material_phbounce_start_velocity
-- r.material_phbouncing = result.result.material_phbouncing
-- r.material_flotation_factor = result.result.material_flotation_factor
-- r.material_shoot_factor = result.result.material_shoot_factor
-- r.material_shoot_factor_mp = result.result.material_shoot_factor_mp
-- r.material_bounce_damage_factor = result.result.material_bounce_damage_factor
-- r.material_injurious_speed = result.result.material_injurious_speed
-- r.material_vis_transparency_factor = result.result.material_vis_transparency_factor
-- r.material_snd_occlusion_factor = result.result.material_snd_occlusion_factor
-- r.material_density_factor = result.result.material_density_factor
-- result.result = r
-- end
return result
end
-- Engine edit required for testing materials
-- If not possible to get material - return nil
function geometry_ray:isMaterialFlag(flag)
local result = self.ray:get_result()
if not result then
return
end
if not result.material_flags then
return
end
if not flags_test[flag] then
return
end
return test(result.material_flags, flags_test[flag])
end
function geometry_ray:getMaterialFlags()
local result = self.ray:get_result()
if not result then
return
end
if not result.material_flags then
return
end
local res = {}
for k, v in pairs(flags_test) do
res[k] = test(result.material_flags, v)
end
return res
end
-- Utils
-- Check if values are similar to a precision
function similar(float1, float2, epsilon)
return math.abs(float1 - float2) <= (epsilon or 0.0001)
end
function vec_similar(vec1, vec2, epsilon)
return similar(vec1.x, vec2.x, epsilon) and similar(vec1.y, vec2.y, epsilon) and similar(vec1.z, vec2.z, epsilon)
end
-- Linear inter/extrapolation
function lerp(a, b, f)
if a and b and f then
return a + f * (b - a)
else
return a or b or 0
end
end
-- Visualize ray from one point to other with particles playing at setted step
class "VisualizeRay"
function VisualizeRay:__init(init_pos, end_pos, particle_step, visualize_time, force_stop)
self.init_pos = init_pos
self.end_pos = end_pos
self.visualize_time = visualize_time or 3000
self.particle_step = particle_step or 0.02
self.force_stop = force_stop
self.force_stop_default = force_stop
self.time = 0
self.start = function()
for i = 0, 1, self.particle_step do
local p = particles_object("amik\\hit_fx\\metal\\hit_sparks_glow")
local x = lerp(self.init_pos.x, self.end_pos.x, i)
local y = lerp(self.init_pos.y, self.end_pos.y, i)
local z = lerp(self.init_pos.z, self.end_pos.z, i)
p:play_at_pos(vector():set(x, y, z))
local time = 0
local stopped = false
AddUniqueCall(function()
if self.time > self.visualize_time then
if not stopped then
if self.force_stop then p:stop() else p:stop_deffered() end
stopped = true
end
if not p:playing() then
p = nil
return true
end
else
if not p:playing() then
p:play_at_pos(vector():set(x, y, z))
end
time = time + device().time_delta
self.time = time
end
end)
end
end
self.start()
self.reset_time = function()
self.time = 0
end
self.reset = function()
self.time = 0
self.force_stop = self.force_stop_default
self.start()
end
self.stop = function()
self.time = self.visualize_time + 1
self.force_stop = true
end
end
-- Get surface normals by Aoldri, edited by demonized
function get_surface_normal(pos, dir)
local ray = geometry_ray({
ray_range = 1000,
visualize = false,
flags = 1+2,
ignore_object = db.actor,
})
-- Get player's camera position and direction in world space
local pos0 = pos and vector():set(pos) or device().cam_pos
local angle1 = dir and vector():set(dir) or device().cam_dir
-- Generate two positions orthogonal to camera direction and each other
local pos01 = vector():set(0, 1, 0)
pos01 = pos01:sub(vector():set(angle1):mul(pos01:dotproduct(angle1)))
pos01:normalize()
local pos02 = vector_cross(angle1, pos01)
pos01 = pos01:mul(0.01)
pos02 = pos02:mul(0.01)
pos01 = pos01:add(pos0)
pos02 = pos02:add(pos0)
-- Get positions of intersections of rays around pos0
local res = ray:get(pos0, angle1)
local pos1 = res.position
local pos2 = ray:get(pos01, angle1).position
local pos3 = ray:get(pos02, angle1).position
if not res.success then
-- print_tip("cant get normal by pos %s, dir %s", pos0, angle1)
return
end
-- VisualizeRay(pos0, pos1, nil, 300)
-- VisualizeRay(pos01, pos2, nil, 300)
-- VisualizeRay(pos02, pos3, nil, 300)
-- Get vectors from intersection points from pos1
local vec2 = vec_sub(pos1, pos2)
local vec3 = vec_sub(pos1, pos3)
-- Find normal vector of surface by taking cross product of intersection vectors
local cross = (vector_cross(vec2, vec3)):normalize()
-- If the direction and normal vectors heading in similar direction - invert normal
local deg = angle1:dotproduct(cross)
if deg > 0 then
cross:invert()
end
-- VisualizeRay(pos1, vector():set(pos1):add(cross), nil, 300)
return cross
end
@@ -0,0 +1,35 @@
-- Allows for a constant location offset, lifting of the bottom plane
-- to prevent collisions with a flat surface, and a central vertex on the bottom plane
class "bbox_collider" (aol_bbox.bbox_collider)
function bbox_collider:__init(width, length, height, world_origin, local_origin, rotation)
super(width, length, height, world_origin, local_origin, rotation)
self:UpdateLengths()
end
function bbox_collider:UpdateVertices()
aol_bbox.bbox_collider.UpdateVertices(self)
local y_offset = 0.02
for i=1,4 do
self.vertices[i].y = self.vertices[i].y +y_offset
end
local crosshair_point = vector():set(0, y_offset, 0)
self.vertices[#self.vertices + 1] = crosshair_point
end
---@param location vector
function bbox_collider:SetCrosshairVertex(location)
self.vertices[#self.vertices] = vector():set(location)
end
---@return vector
function bbox_collider:GetCrosshairVertex()
return vector():set(self.vertices[#self.vertices])
end
function bbox_collider:OffsetVertices(vector_offset)
for i, vertex_pos in ipairs(self.vertices) do
vertex_pos:add(vector_offset)
end
end
@@ -0,0 +1,53 @@
-- Draw a sphere as the crosshair vertex (point of rotation)
class "bshape_renderer" (aol_bshape.bshape_renderer)
function bshape_renderer:__init() super()
self.crosshair_gizmo = nil
end
function bshape_renderer:DrawCube(bshape_collider)
if self.crosshair_gizmo == nil then
self.crosshair_gizmo = self:AddCrosshairGizmo(bshape_collider)
end
aol_bshape.bshape_renderer.DrawCube(self, bshape_collider)
local scale_mat = matrix():identity():scale(0.02,0.02,0.02)
local pos_mat = matrix():translate(bshape_collider.world_vertices[self.crosshair_gizmo.vertex])
local mat = matrix():mul(pos_mat, scale_mat)
self.crosshair_gizmo.gizmo.matrix = mat
self.crosshair_gizmo.gizmo.visible = true
end
function bshape_renderer:AddCrosshairGizmo(bshape_collider)
if bshape_collider == nil then
return
end
local crosshair_vertex = #bshape_collider.world_vertices
local color = aol_bshape.color_not_colliding
aol_bshape.last_gizmo_id = aol_bshape.last_gizmo_id + 1
local sphere = debug_render.add_object(aol_bshape.last_gizmo_id, DBG_ScriptObject.sphere):cast_dbg_sphere()
sphere.visible = true
sphere.color = color
local scale_mat = matrix():identity():scale(0.02,0.02,0.02)
local pos_mat = matrix():translate(bshape_collider.world_vertices[crosshair_vertex])
local mat = matrix():mul(pos_mat, scale_mat)
sphere.matrix = mat
return {
gizmo = sphere,
vertex = #bshape_collider.world_vertices,
}
end
function bshape_renderer:StopCrosshairGizmo()
self.crosshair_gizmo.gizmo.visible = false
end
function bshape_renderer:Stop()
aol_bshape.bshape_renderer.Stop(self)
self:StopCrosshairGizmo()
end
@@ -0,0 +1,103 @@
-- Get object sections
local object_sections = ui_debug_main.get_spawn_table("Physic (Misc.)")
print_table(object_sections)
-- GUI Class
class "UIHFSelectObj" (CUIScriptWnd)
function UIHFSelectObj:__init() super()
self:InitControls()
self:InitCallbacks()
end
function UIHFSelectObj:InitControls()
self:SetWndRect(Frect():set(0, 0, 1024, 768))
local xml = CScriptXmlInit()
-- xml:ParseFile("ui_sleep_dialog.xml")
xml:ParseFile("ui_hf_creativemode_dialog.xml")
-- List of objects
self.obj_select = xml:InitComboBox("obj_select", self)
self:Register(self.obj_select, "obj_select")
for i=1,#object_sections do
self.obj_select:AddItem(object_sections[i], i)
end
-- Spawn button
self.btn_spawn = xml:Init3tButton("btn_spawn", self)
self:Register(self.btn_spawn, "btn_spawn")
end
function UIHFSelectObj:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if (res == false) then
local bind = dik_to_bind(dik)
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
self:Close()
elseif dik == DIK_keys.DIK_RETURN then
self:Accept()
end
end
end
return res
end
function UIHFSelectObj:InitCallbacks()
self:AddCallback("btn_spawn", ui_events.BUTTON_CLICKED, self.Accept, self)
end
function UIHFSelectObj:Update()
CUIScriptWnd.Update(self)
end
function UIHFSelectObj:Accept()
local i = self.obj_select:CurrentID()
local section = object_sections[i]
if not section then return end
placeable_furniture.start_placing_item(section)
self:Close()
end
function UIHFSelectObj:Close()
self:HideDialog()
Unregister_UI("UIHFSelectObj")
end
-- GUI
---@type hf_creativemode.UIHFSelectObj
GUISelect = nil
function open_select_menu()
if not DEV_DEBUG then return end
hide_hud_inventory()
if (not GUISelect) then
GUISelect = UIHFSelectObj()
end
if (GUISelect) and (not GUISelect:IsShown()) then
GUISelect:ShowDialog(true)
Register_UI("UIHFSelectObj","creative_mode")
end
end
--
local bind_open_menu = DIK_keys.DIK_SEMICOLON
function on_key_press(dik)
if dik ~= bind_open_menu then return end
open_select_menu()
end
function on_option_change(mcm)
if not mcm then return end
bind_open_menu = ui_mcm.get("aol_hf/debug/bind_creative_mode") or bind_open_menu
end
function on_game_start()
RegisterScriptCallback("on_option_change",on_option_change)
on_option_change(ui_mcm and ui_mcm.key_hold)
RegisterScriptCallback("on_key_press", on_key_press)
end
@@ -0,0 +1,153 @@
local TYPE2FUNC = {}
---@param obj game_object|cse_alife_object|string
---@return string|nil type `nil` on failure
function get_type(obj)
-- Get type directly from section name
if type(obj) == "string" then
return SYS_GetParam(0, obj, "placeable_type")
end
local item_section = nil
-- Get type from game object
if type(obj.id) == "function" then
---@cast obj game_object
local type = SYS_GetParam(0, obj:section(), "placeable_type")
if type then return type end
item_section = SYS_GetParam(0, obj:section(), "item_section")
-- Get type from server object
elseif type(obj.id) == "number" then
---@cast obj cse_alife_object
local type = SYS_GetParam(0, obj:section_name(), "placeable_type")
if type then return type end
item_section = SYS_GetParam(0, obj:section_name(), "item_section")
end
if not item_section then return end
-- Check item variant for placeable_type
return SYS_GetParam(0, item_section, "placeable_type")
end
function add_type(type, func, force)
if TYPE2FUNC[type] and not force then return end
TYPE2FUNC[type] = func
end
function get_func(type)
return TYPE2FUNC[type]
end
function init_light(obj_id)
-- Modify light to values specified in section
modify_light_data(obj_id)
-- Switch off light when it is online
-- CreateTimeEvent("PlaceableLight","TurnOffLight" .. obj_id, 0, turn_off_light, obj_id)
hf_obj_manager.update_data(obj_id, {is_on=false})
end
add_type("light", init_light)
function init_stash(obj_id)
local item_section = ini_sys:r_string_ex(alife_object(obj_id):section_name(), "item_section")
if not item_section then return end
-- Save stash data
local m_data = alife_storage_manager.get_state()
if not (m_data.player_created_stashes) then
m_data.player_created_stashes = {}
end
m_data.player_created_stashes[obj_id] = item_section
-- Send script callback (not used in vanilla)
local data = {
stash_id = obj_id,
stash_name = item_section,
stash_section = item_section
}
SendScriptCallback("actor_on_stash_create", data)
end
add_type("stash", init_stash)
----
-- Light Functions
----
function modify_light_data(obj_id)
local obj = alife_object(obj_id)
if not obj then return end
local section = obj:section_name()
local data = utils_stpk.get_lamp_data(obj)
local light_section = ini_sys:r_string_ex(section, "light_section", "glowstick_definition")
data.visual_name = ini_sys:r_string_ex(section, "visual", "dynamics\\placeable\\gas_lamp")
-- Type of light
local type = ini_sys:r_float_ex(light_section, "type", 2)
if type == 1 then -- Point
data.light_flags = 45
elseif type == 2 then -- Spot
data.light_flags = 61
end
-- Add shadows
if (ini_sys:r_bool_ex(light_section, "has_shadows", false)) then
data.light_flags = data.light_flags + 2
end
-- Volumetric light
if (ini_sys:r_bool_ex(light_section, "volumetric", false)) then
data.light_flags = data.light_flags + 64
data.volumetric_quality = ini_sys:r_float_ex(light_section, "volumetric_quality", 0.5)
data.volumetric_intensity = ini_sys:r_float_ex(light_section, "volumetric_intensity", 0.5)
data.volumetric_distance = ini_sys:r_float_ex(light_section, "volumetric_distance", 0.5)
end
-- Color
local color_s = ini_sys:r_string_ex(light_section, "color", "0.5,0.5,0.5,0.5")
local _s = str_explode(color_s,",")
for i=1,#_s do
_s[i] = clamp(round(tonumber(_s[i]) * 255), 0, 255)
end
local function lshift(x, by)
return x * 2 ^ by
end
data.main_color = lshift(bit_and(_s[4],0xff), 24) + lshift(bit_and(_s[1],0xff), 16) + lshift(bit_and(_s[2],0xff), 8) + bit_and(_s[3],0xff)
data.main_brightness = 1.0
data.main_color_animator = ini_sys:r_string_ex(light_section, "color_animator", "empty")
data.main_bone = ini_sys:r_string_ex(light_section, "main_bone", "bone_light")
data.main_cone_angle = ini_sys:r_float_ex(light_section, "cone_angle", 0)
data.main_range = ini_sys:r_float_ex(light_section, "range", 15)
utils_stpk.set_lamp_data(data, obj)
return true
end
function turn_off_light(obj_id)
local obj = get_object_by_id(obj_id)
if not (obj) then
return
end
if not (obj.get_hanging_lamp) then
return
end
local lamp = obj:get_hanging_lamp()
if not lamp then
return
end
if lamp:is_on() then lamp:turn_off() end
return true
end
@@ -0,0 +1,282 @@
local mcm_keybinds = ui_mcm and ui_mcm.key_hold
local record_on_place = false
local record_on_keypress = false
local bind_record = DIK_keys.DIK_APOSTROPHE
---@class UIRecordObj
GUI = nil -- instance, don't touch
function add_obj_prompt(section, position, level_vertex_id, game_vertex_id, rotation)
if not DEV_DEBUG then
return
end
hide_hud_inventory()
if (not GUI) then
GUI = UIRecordObj()
end
if (GUI) and (not GUI:IsShown()) then
GUI:ShowDialog(true)
GUI:Reset(section, position, level_vertex_id, game_vertex_id, rotation)
Register_UI("UIRecordObj","hf_record_on_place")
end
end
------------------------------------------------------------------
-- UI
-------------------------------------------------------------------
class "UIRecordObj" (CUIScriptWnd)
function UIRecordObj:__init() super()
self:InitControls()
self:InitCallBacks()
end
function UIRecordObj:__finalize()
end
function UIRecordObj:InitControls()
self:SetWndRect(Frect():set(0,0,1024,768))
self:SetAutoDelete(true)
--self:Enable(true)
local xml = CScriptXmlInit()
xml:ParseFile ("ui_items_backpack.xml")
self.dialog = xml:InitStatic("backpack", self)
xml:InitStatic("backpack:background", self.dialog)
self.input = xml:InitEditBox("backpack:input",self.dialog)
self:Register(self.input,"fld_input")
local btn = xml:Init3tButton("backpack:btn_cancel", self.dialog)
self:Register(btn,"btn_cancel")
btn = xml:Init3tButton("backpack:btn_ok", self.dialog)
self:Register(btn,"btn_ok")
end
function UIRecordObj:InitCallBacks()
self:AddCallback("btn_ok", ui_events.BUTTON_CLICKED, self.OnAccept, self)
self:AddCallback("btn_cancel", ui_events.BUTTON_CLICKED, self.Close, self)
end
function UIRecordObj:Reset(section, position, level_vertex_id, game_vertex_id, rotation)
self.input:SetText("")
self.section = section
self.position = position
self.level_vertex_id = level_vertex_id
self.game_vertex_id = game_vertex_id
self.rotation = rotation
end
function UIRecordObj:Update()
CUIScriptWnd.Update(self)
end
function UIRecordObj:OnAccept()
local spawn_name = self.input:GetText()
local data = {
["object"] = self.section,
["position"] = self.position,
["lvid"] = self.level_vertex_id,
["gvid"] = self.game_vertex_id,
["rotation"] = self.rotation,
}
add_obj_to_ltx(spawn_name, data)
self:Close()
end
function UIRecordObj:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if (res == false) then
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
self:Close()
end
end
end
return res
end
function UIRecordObj:Close()
self:HideDialog()
Unregister_UI("UIRecordObj")
end
----------------------------------
-- Storing object data to LTX
----------------------------------
local function is_vector(var)
if type(var) ~= "userdata" then return false end
if var.x and var.y and var.z then return true end
return false
end
function add_obj_to_ltx(spawn_name, data)
---@type ini_file_ex
local ini_cc = ui_debug_launcher.ini_cc
if spawn_name == nil or spawn_name == "" then
spawn_name = os.date("%Y/%m/%d-%H:%M:%S")
end
for key, value in pairs(data) do
ini_cc:w_value(spawn_name, key, is_vector(value) and utils_data.vector_to_string(value) or value)
end
ini_cc:save()
end
----------------------------------
-- Spawning objects on new game
----------------------------------
---A map of spawned objects, pointing to their object id if object still exists, true if released, nil otherwise
---@type table<string, number|true>
spawned_objects = {}
-- Map of object ids to spawn id (used for cleanup)
id_to_spawn_id = {}
---Generates a list of objects to be spawned, reading from objects.ltx (old format) and spawns.ltx (new format)
---@return {section: string, location: vector, lvid: number, gvid: number, rotation: vector, spawn_id: number, story_id: string|nil, data: table}[]
function generate_spawn_list()
local obj_tbl = {}
-- Iterate over objects.ltx, reading every entry for every map (section)
local ini_map_objects = ini_file_ex("plugins\\world_objects\\objects.ltx")
local j = 1
for _, section in pairs(ini_map_objects:get_sections()) do
local n = ini_map_objects.ini:line_count(section)
for i=0,n-1 do
local result, id, value = ini_map_objects.ini:r_line_ex(section,i)
local p = str_explode(value,",")
if (p) then
obj_tbl[j] = {
section = p[1],
location = vector():set(tonumber(p[2]), tonumber(p[3]), tonumber(p[4])),
lvid = tonumber(p[5]),
gvid = tonumber(p[6]),
rotation = vector():set(tonumber(p[7]), tonumber(p[8]), tonumber(p[9])),
spawn_id = id,
}
j = j + 1
end
end
end
-- Iterate over spawns.ltx, reading every entry (section)
local ini_spawns = ini_file_ex("plugins\\world_objects\\spawns.ltx")
for _, section in pairs(ini_spawns:get_sections()) do
local entry = {
section = ini_spawns:r_string_ex(section, "object"),
location = utils_data.string_to_vector(ini_spawns:r_string_ex(section, "position")),
rotation = utils_data.string_to_vector(ini_spawns:r_string_ex(section, "rotation")),
lvid = ini_spawns:r_float_ex(section, "lvid"),
gvid = ini_spawns:r_float_ex(section, "gvid"),
spawn_id = section,
story_id = ini_spawns:r_string_ex(section, "story_id"),
data = parse_key_value(ini_spawns:r_string_ex(section, "data"))
}
table.insert(obj_tbl, entry)
end
return obj_tbl
end
function actor_on_first_update()
-- Spawn world objects
for k, v in pairs(generate_spawn_list()) do
if spawned_objects[v.spawn_id] then goto continue end
local obj_id = placeable_furniture.create_object(v.section, v.location, v.rotation, v.lvid, v.gvid)
if not obj_id then goto continue end
-- Set world_obj flag: Disable pickup
hf_obj_manager.update_data(obj_id, {is_world_obj=true})
-- Track spawned objects
spawned_objects[v.spawn_id] = obj_id
id_to_spawn_id[obj_id] = v.spawn_id
-- Register story_id if it exists
if v.story_id then
story_objects.register(obj_id, v.story_id)
end
-- Set custom data
if v.data then
for i,val in pairs(v.data) do
v.data[i] = tonumber(val) or val
end
hf_obj_manager.update_data(obj_id, v.data)
end
::continue::
end
end
function save_state(m_data)
m_data.spawned_objects = spawned_objects
end
function load_state(m_data)
spawned_objects = m_data.spawned_objects or spawned_objects
end
function on_key_press(dik)
if dik ~= bind_record or not record_on_keypress then return end
---@type game_object
local obj = placeable_furniture.get_obj_at_crosshair()
if not obj then return end
local bone_name = obj:bone_name(0)
local angle_hpb = obj:bone_direction(bone_name)
local true_angle = vector():set(angle_hpb.y, angle_hpb.x, angle_hpb.z)
add_obj_prompt(obj:section(), obj:position(), obj:level_vertex_id(), obj:game_vertex_id(), true_angle)
end
function on_option_change(mcm) --new in mcm 1.6.0 mcm passes true to the on_option_change callback
if mcm then
record_on_place = ui_mcm.get("aol_hf/world_obj/flag_record_on_place")
record_on_keypress = ui_mcm.get("aol_hf/world_obj/flag_record_on_keypress")
bind_record = ui_mcm.get("aol_hf/world_obj/bind_record") or bind_record
end
end
-- Open record prompt if MCM option is on
function hf_on_furniture_place(id)
if not record_on_place then return end
local obj = alife_object(id)
if not obj then return end
add_obj_prompt(obj:section_name(), obj.position, obj.m_level_vertex_id, obj.m_game_vertex_id, obj.angle)
end
function server_entity_on_unregister(se_obj,type_name)
local spawn_id = id_to_spawn_id[se_obj.id]
if not spawn_id then return end
spawned_objects[spawn_id] = true
id_to_spawn_id[se_obj.id] = nil
end
function on_game_start()
RegisterScriptCallback("on_option_change",on_option_change)
on_option_change(mcm_keybinds)
RegisterScriptCallback("on_key_press", on_key_press)
RegisterScriptCallback("actor_on_first_update",actor_on_first_update)
RegisterScriptCallback("save_state",save_state)
RegisterScriptCallback("load_state",load_state)
RegisterScriptCallback("hf_on_furniture_place", hf_on_furniture_place)
RegisterScriptCallback("server_entity_on_unregister", server_entity_on_unregister)
end
@@ -0,0 +1,52 @@
local mcm_keybinds = ui_mcm and ui_mcm.key_hold
function on_mcm_load()
local options = {
id = "aol_hf",
gr = {
{
id = "controls",
sh = true,
gr = {
{id = "ctrl_title", type = "slide", link = "ui_options_slider_player", text = "ui_mcm_menu_controls", size = {512, 50}, spacing = 20},
{id = "bind_collision", type = "key_bind", val = 2, def=DIK_keys.DIK_TAB},
{id = "bind_alignment", type = "key_bind", val = 2, def=DIK_keys.DIK_CAPITAL},
{id = "bind_place_mode", type = "key_bind", val = 2, def=DIK_keys.DIK_HOME},
}
},
{
id = "gameplay",
sh = true,
gr = {
{id = "game_title", type = "slide", link = "ui_options_slider_player", text = "ui_mcm_menu_gameplay", size = {512, 50}, spacing = 20},
{id = "uniform_trade_profiles", type = "check", val = 1, def=false},
{id = "use_ingame_time", type = "check", val = 1, def=false},
}
},
{
id = "world_obj",
sh = true,
gr = {
{id = "world_obj_title", type = "slide", link = "ui_options_slider_player", text = "ui_mcm_menu_world_obj", size = {512, 50}, spacing = 20},
{id = "flag_record_on_place", type = "check", val = 1, def=false},
{id = "flag_record_on_keypress", type = "check", val = 1, def=false},
{id = "bind_record", type = "key_bind", val = 2, def=DIK_keys.DIK_APOSTROPHE},
}
},
{
id = "debug",
sh = true,
gr = {
{id = "debug_title", type = "slide", link = "ui_options_slider_player", text = "ui_mcm_menu_debug", size = {512, 50}, spacing = 20},
{id = "bind_creative_mode", type = "key_bind", val = 2, def=DIK_keys.DIK_SEMICOLON},
{id = "location_offset", type = "input", val = 0, def="0,0,0"},
}
}
}
}
return options
end
@@ -0,0 +1,76 @@
local stash_sections = {}
local function itr_for_stashes(section)
local placeable_type = ini_sys:r_string_ex(section, "placeable_type") or "prop"
if placeable_type == "stash" then
stash_sections[section] = true
end
end
ini_sys:section_for_each(itr_for_stashes)
local function remove_stash(box)
if not box then return end
if (stash_sections[box:section()]) then
if (box:is_inv_box_empty()) then
hide_hud_inventory()
local data = {
stash_id = box:id(),
cancel = false,
}
SendScriptCallback("actor_on_stash_remove",data)
if data.cancel then
return
end
local id = box:id()
level.map_remove_object_spot(id, "treasure")
local se_obj = alife_object(id)
if se_obj then
alife_release(se_obj)
end
local m_data = alife_storage_manager.get_state()
if (m_data.player_created_stashes and m_data.player_created_stashes[id]) then
local section = m_data.player_created_stashes[id]
alife_create_item(section, db.actor)
m_data.player_created_stashes[id] = nil
end
end
end
end
function ui_inventory.UIInventory:LMode_TakeAll()
self:Print(nil, "LMode_TakeAll")
local npc = self:GetPartner()
if (not npc) then
return
end
local cc = self.CC["npc_bag"]
if (not cc) then
return
end
local has_items = false
for id,idx in pairs(cc.indx_id) do
has_items = true
local ci = cc:GetCell_ID(id)
if ci then
-- Transfer item
local obj = level.object_by_id(id)
if self:Cond_Move(obj, "npc_bag") then
self:Action_Move(obj, "npc_bag")
end
end
end
-- Update info
self.update_info = true
if not has_items then
local stash = self:GetPartner()
remove_stash(stash)
end
end
@@ -0,0 +1,63 @@
local varname = "hf_data"
-- Soft update
function update_data(id, data)
local hf_data = se_load_var(id, nil, varname) or {}
for index, datum in pairs(data) do
hf_data[index] = datum
end
se_save_var(id, nil, varname, hf_data)
end
-- Override
function set_data(id, data)
se_save_var(id, nil, varname, data)
end
-- Getter
function get_data(id) -- need to add code to binders to update data before access
return se_load_var(id, nil, varname)
end
-- Destroy
function delete_data(id)
se_save_var(id, nil, varname, nil)
end
-- Debugging print
function print_data(id)
printf("print_data(id):"..id)
local hf_data = se_load_var(id, nil, varname)
if not hf_data then
printf("no data")
return
end
for index, datum in pairs(hf_data) do
printf("index:"..index..", datum:"..datum)
end
end
function save_state(mdata)
-- Try to save data from online objects
local function save_data(obj)
local wrapper = bind_hf_base.get_wrapper(obj:id())
if not wrapper then return end
if not wrapper.save_data then return end
wrapper:save_data()
end
for obj in game_objects_iter() do
save_data(obj)
end
end
function cleanup_data(id)
SendScriptCallback("hf_on_before_furniture_release", id)
delete_data(id)
end
function on_game_start()
RegisterScriptCallback("save_state",save_state)
end
@@ -0,0 +1,103 @@
-- Display onscreen text for placement controls
local function get_placement_binds()
return {
["key_interact"] = ui_mcm.dispaly_key(bind_to_dik(key_bindings.kUSE)),
["key_collision"] = ui_mcm.dispaly_key(ui_mcm.get("aol_hf/controls/bind_collision")),
["key_align"] = ui_mcm.dispaly_key(ui_mcm.get("aol_hf/controls/bind_alignment")),
["key_mode"] = ui_mcm.dispaly_key(ui_mcm.get("aol_hf/controls/bind_place_mode")),
}
end
local function generate_msg(string_id)
local msg = game.translate_string(string_id)
return utils_data.parse_string_keys(msg, get_placement_binds())
end
local function generate_holding_msg()
return generate_msg("st_hf_holding_msg")
end
local function generate_advanced_msg()
return generate_msg("st_hf_advanced_msg")
end
local function generate_preview_msg()
return generate_msg("st_hf_preview_msg")
end
local state_to_msg_func = {
[placeable_furniture.states.HOLDING] = generate_holding_msg,
[placeable_furniture.states.ADV_CTRL] = generate_advanced_msg,
[placeable_furniture.states.PREVIEW] = generate_preview_msg
}
---@type hf_placement_hud.UIHFPlacementHUD|nil
HUD = nil
class "UIHFPlacementHUD" (CUIScriptWnd)
function UIHFPlacementHUD:__init() super()
self:Initialise()
end
function UIHFPlacementHUD:__finalize()
end
function UIHFPlacementHUD:Initialise()
self:SetWndRect(Frect():set(0,0,1024,768))
self:SetAutoDelete(true)
local xml = CScriptXmlInit()
xml:ParseFile("ui_hf_placement_hud.xml")
self.bg = xml:InitFrame("background", self)
self.text = xml:InitTextWnd("background:text", self.bg)
end
function UIHFPlacementHUD:Refresh(state)
local msg_func = state_to_msg_func[state]
if not msg_func then self:Clear() end
self.text:SetText(msg_func())
self.text:AdjustHeightToText()
self.bg:SetHeight(self.text:GetHeight() + 24)
end
function UIHFPlacementHUD:Update()
CUIScriptWnd.Update(self)
end
function UIHFPlacementHUD:Clear()
self.text:SetText("")
end
-------------
function update_hud(state)
if HUD == nil then
HUD = UIHFPlacementHUD()
get_hud():AddDialogToRender(HUD)
end
HUD:Refresh(state)
end
function remove_hud()
if HUD ~= nil then
get_hud():RemoveDialogToRender(HUD)
HUD = nil
end
end
local hf_set_state = placeable_furniture.set_state
function placeable_furniture.set_state(new_state)
hf_set_state(new_state)
if placeable_furniture.in_placing_state() then
update_hud(new_state)
else
remove_hud()
end
end
@@ -0,0 +1,147 @@
VERSION = "2.3.2"
---- `0` = The value 0 if `version1` is the same as `version2`
---- `-1` = The value less than 0 if `version1` is older than `version2`
---- `1` = The value greater than 0 if `version1` newer than `version2 `
---@param version1 string
---@param version2 string
---@return -1|0|1
local function compare_version_to(version1, version2)
local major1, minor1, patch1 = version1:match("(%d+)%.(%d+)%.(%d+)")
local major2, minor2, patch2 = version2:match("(%d+)%.(%d+)%.(%d+)")
-- There's probably a better way, but I'm lazy atm :P
if major1 > major2 then return 1 end
if major1 < major2 then return -1 end
if minor1 > minor2 then return 1 end
if minor1 < minor2 then return -1 end
if patch1 > patch2 then return 1 end
if patch1 < patch2 then return -1 end
return 0
end
------------------------
-- Migration Functions
------------------------
-- 2.2.1 -> 2.3.0
-- Make displayed guns invisible to AI, stopping them from picking them up
local function fix_racked_weapons()
if not weapon_showcase then return end
for case_id, item_ids in pairs(weapon_showcase.weapon_displays) do
for item_id, display_id in pairs(item_ids) do
local world_se_obj = alife_object(display_id)
local data = utils_stpk.get_weapon_data(world_se_obj)
local remove_flags = 16
local flag_mask = bit_not(remove_flags)
data.object_flags = bit_and(data.object_flags, flag_mask)
utils_stpk.set_weapon_data(data, world_se_obj)
end
end
end
-- 2.1.X -> 2.2.0
-- Release all world objects to migrate old saves to use the
-- per-object spawning system instead
local function migrate_world_objects()
local hf_obj_spawned = alife_storage_manager.get_state().hf_obj_spawned
if not hf_obj_spawned then return end
alife():iterate_objects(function(se_obj)
local data = hf_obj_manager.get_data(se_obj.id)
if data and data.is_world_obj then
alife_release(se_obj, "[HF] Releasing old world object with id " .. se_obj.id)
end
end)
alife_storage_manager.get_state().hf_obj_spawned = nil
end
-- pre-2.0.0
-- Initialise metadata
local function reinit_obj_data()
local sim = alife()
for i=1,65534 do
local se_obj = sim:object(i)
if se_obj then
local placeable_type = hf_furniture_types.get_type(se_obj)
if placeable_type then
-- Initialise data store
hf_obj_manager.set_data(se_obj.id, {})
if placeable_type == "light" then
-- Switch off light when it is online
hf_obj_manager.update_data(obj_id, {is_on=false})
end
end
end
end
end
----------------------------------------------------------------
-- Generic functions to migrate save based on semver versioning
----------------------------------------------------------------
-- Array of functions for migrating from old versions
-- Assumed to be sorted already
local migration_functions = {
{
version = "2.1.0",
functor = reinit_obj_data,
},
{
version = "2.2.0",
functor = migrate_world_objects,
},
{
version = "2.3.0",
functor = fix_racked_weapons,
}
}
---@return number|nil
local function get_starting_migration_i(version)
for i, migration_function_data in ipairs(migration_functions) do
-- Exit if current version is older than a version with migrate function
if compare_version_to(version, migration_function_data.version) == -1 then
return i
end
end
end
---@param version string
local function migrate_save(version)
local start_i = get_starting_migration_i(version)
if not start_i then return end
for i=start_i, #migration_functions do
migration_functions[i].functor()
end
end
-------------
-- Callbacks
-------------
function save_state(mdata)
mdata.hf_version = VERSION
end
function actor_on_init()
local save_hf_version = alife_storage_manager.get_state().hf_version
if not save_hf_version or save_hf_version ~= VERSION then
-- Save has a different version from that which is loaded
migrate_save(save_hf_version or "0.0.0")
end
end
function on_game_start()
RegisterScriptCallback("save_state", save_state)
RegisterScriptCallback("actor_on_init", actor_on_init)
end
@@ -0,0 +1,930 @@
gc = game.translate_string
-- util stuff
local function pr(...)
local debug = true
if debug then
printf("placeable_furniture: " .. ...)
end
end
local function print_msg(string_id)
local msg = gc(string_id)
if ui_popup_messages then
ui_popup_messages.GUI:AddMessage(msg)
else
actor_menu.set_msg(1, msg, 3)
end
end
-- keybinds
local mcm_keybinds = ui_mcm and ui_mcm.key_hold
local key_place = bind_to_dik(key_bindings.kUSE)
local key_toggle_collision = DIK_keys.DIK_TAB
local key_toggle_align = DIK_keys.DIK_CAPITAL
local key_toggle_controls = DIK_keys.DIK_HOME
states = {
IDLE=1,
HOLDING=2,
ADV_CTRL=3,
PREVIEW=4
}
local state = states.IDLE
align_states = {
ACTOR = 1,
SURFACE = 2,
}
align_state = align_states.ACTOR
local check_collision = true
local place_coordinates = nil
local place_sound = xr_sound.get_safe_sound_object( "interface\\place_object" )
local item_id = nil
local phy_obj_section = nil
local location_offset = vector():set(0, 0, 0)
local rotation_offset = 0
local base_rot = 0
local base_loc = vector():set(0, 0, 0)
local player_pos = nil
local player_dir = nil
local player_dist = nil
local bbox = nil
local bbox_drawer = hf_bshape.bshape_renderer()
---------------------
-- State Management
---------------------
function reset_offsets()
location_offset = utils_data.string_to_vector(ui_mcm.get("aol_hf/debug/location_offset"))
rotation_offset = 0
end
transition_functors = {
[states.HOLDING] = {
[states.ADV_CTRL] = function ()
player_pos = vector():set(device().cam_pos)
player_dir = vector():set(device().cam_dir)
player_dist = level.get_target_dist()
end
},
[states.ADV_CTRL] = {
[states.HOLDING] = reset_offsets
},
[states.PREVIEW] = {
[states.HOLDING] = reset_offsets
},
[states.IDLE] = {
[states.HOLDING] = reset_offsets
}
}
-- Perform some function when a state transitions occurs
function on_state_transition(old_state, new_state)
local func = transition_functors[old_state] and transition_functors[old_state][new_state]
if func then
func()
end
end
function get_state()
return state
end
function set_state(new_state)
local old_state = state
state = new_state
on_state_transition(old_state, new_state)
end
function is_state(check_state)
return state == check_state
end
function in_placing_state()
return get_state() ~= states.IDLE
end
---------------------
-- Objects and their data
---------------------
---Creates and places an object in the world.
---Also initialises additional data depending on their placeable_type as defined in their section
---@param placeable_section string
---@param location vector
---@param rotation aol_rotation.Quaternion|vector
---@param lvid integer? Level Vertex ID
---@param gvid integer? Game Vertex ID
---@return integer|nil obj.id
function create_object(placeable_section, location, rotation, lvid, gvid)
-- Exit early if placeable_section is not supplied
if not placeable_section then
pr("Could not find associated object with item")
return
end
location = location or vector():set(0,0,0)
rotation = rotation or vector():set(0,0,0)
local obj = alife_create(placeable_section, location, lvid or db.actor:level_vertex_id(), gvid or db.actor:game_vertex_id())
if not obj then return end
-- Rotate object with quaternion or vector
if rotation.w then
obj.angle = rotation:to_euler_angles()
else
obj.angle = vector():set(rotation.x, rotation.y, rotation.z)
end
-- Initialise data store
hf_obj_manager.set_data(obj.id, {})
-- Initialise additional data
local placeable_type = ini_sys:r_string_ex(placeable_section, "placeable_type") or "prop"
local type_functor = hf_furniture_types.get_func(placeable_type)
if type_functor then
type_functor(obj.id)
end
-- Remove flags
local data = utils_stpk.get_physic_data(obj)
local remove_flags = 128
local flag_mask = bit_not(remove_flags)
data.object_flags = bit_and(data.object_flags, flag_mask)
utils_stpk.set_physic_data(data, obj)
SendScriptCallback("hf_on_furniture_spawn", obj.id)
return obj.id
end
function transfer_item_data(item_id, obj_id)
if not item_id or not obj_id then return end
-- Copy data from item to obj and delete item data
local hf_data = hf_obj_manager.get_data(item_id)
if hf_data then
hf_obj_manager.update_data(obj_id, hf_data)
hf_obj_manager.delete_data(item_id)
end
-- Update condition if item is online
local item = get_object_by_id(item_id)
if not item then return end
hf_obj_manager.update_data(obj_id, {condition=item:condition()})
end
---------------------
-- Placement System
---------------------
local function actor_on_update()
if not in_placing_state() then return end
local cam_pos = nil
local cam_dir = nil
local dist = nil
if is_state(states.HOLDING) then
cam_pos = device().cam_pos
cam_dir = device().cam_dir
dist = level.get_target_dist()
else
cam_pos = player_pos
cam_dir = player_dir
dist = player_dist
end
-- get position of point that the player is looking at
local pos = vector()
pos:mad(cam_pos, cam_dir,dist)
-- rotate to point towards player + offset
if align_state == align_states.ACTOR then
local rot = cam_dir:getH() + (((base_rot+rotation_offset) * math.pi) / 180)
local q0 = aol_rotation.get_rotation_around(vector():set(0, 1, 0), rot)
bbox.rotation = q0
local angle = cam_dir:getH()
-- Rotate vector about y
local c = math.cos (angle)
local s = math.sin (angle)
local rotated_x = location_offset.x * c - location_offset.z * s
local rotated_z = location_offset.x * s + location_offset.z * c
pos:add(vector():set(rotated_x,
location_offset.y,
rotated_z))
-- automatic alignment to surface
elseif align_state == align_states.SURFACE then
-- Rotation
local rot_z = ((base_rot+rotation_offset) * math.pi) / 180
local u = vector():set(0, 1, 0)
local v = demonized_geometry_ray.get_surface_normal(cam_pos, cam_dir)
if not v then return end
-- rotate from upwards vector to normal vector on surface
local q0 = aol_rotation.get_rotation_between(u, v)
-- rotate to point downwards + offset
local angle_to_downwards = 0
local similarity_to_upwards_axis = v:dotproduct(vector():set(0,1,0))
if similarity_to_upwards_axis > 0.9999 then -- pointing upwards
angle_to_downwards = cam_dir:getH()
elseif similarity_to_upwards_axis < -0.9999 then -- pointing downwards
angle_to_downwards = math.pi - cam_dir:getH()
else
angle_to_downwards = angle_to_downwards + v:getH() + math.pi
end
local q1 = aol_rotation.get_rotation_around(v, angle_to_downwards + rot_z)
-- perform each rotation in succession
local q = aol_rotation.Quaternion():multiply(q0):multiply(q1)
-- Location
local q1_2 = aol_rotation.get_rotation_around(v, angle_to_downwards)
local q_loc = aol_rotation.Quaternion():multiply(q0):multiply(q1_2)
local rotated_loc_offset = q_loc:rotate_vector(location_offset)
bbox.rotation = q
pos:add(rotated_loc_offset)
end
bbox:SetWorldOrigin(vector():set(pos))
-- Update position of vertices in bounding box
bbox:UpdateBBox()
if check_collision then
bbox:CheckForCollisions()
else
bbox.is_colliding = false
end
bbox_drawer:DrawBShapeCollider(bbox)
place_coordinates = pos
end
local direction_keys = {
[key_bindings.kFWD] = true,
[key_bindings.kBACK] = true,
[key_bindings.kL_STRAFE] = true,
[key_bindings.kR_STRAFE] = true,
[key_bindings.kCROUCH] = true,
[key_bindings.kACCEL] = true,
[key_bindings.kWPN_FIRE] = true,
[key_bindings.kWPN_ZOOM] = true,
[key_bindings.kL_LOOKOUT] = true,
[key_bindings.kR_LOOKOUT] = true,
[key_bindings.kJUMP] = true,
[key_bindings.kQUIT] = true,
[7] = true,
[key_bindings.kCONSOLE] = true,
}
---@return game_object|nil
function get_obj_at_crosshair()
if level.get_target_dist() > 5 then return end
local obj = level.get_target_obj()
if not obj then return end
return obj
end
---@return bind_hf_base.hf_binder_wrapper|nil
function get_wrapper_at_crosshair()
local obj = get_obj_at_crosshair()
if not obj then return end
local binder = obj:binded_object()
if not binder then return end
local wrapper = binder.wrapper
if not wrapper then return end
return wrapper
end
local function on_key_hold(dik)
if dik_to_bind(dik) ~= key_bindings.kUSE then return end
if Check_UI("UIRadialMenu") then return end
if not in_placing_state() then
if ui_mcm.key_hold("hf_interact_adv", dik) then
local wrapper = get_wrapper_at_crosshair()
if not wrapper then return end
if Check_UI() then return end
open_interact_gui(wrapper)
end
end
end
local function on_key_press(dik)
if dik_to_bind(dik) == key_bindings.kUSE then
if Check_UI("UIRadialMenu") then return end
if not in_placing_state() then
ui_mcm.simple_press("hf_interact_simple", dik, function ()
local wrapper = get_wrapper_at_crosshair()
if not wrapper then return end
wrapper:use_callback_simple()
end)
end
end
if dik == key_place then
if in_placing_state() then
-- Prevent placement if target pos is too far away
if level.get_target_dist() > 5 and not DEV_DEBUG then
print_msg("st_far_popup")
pr("Cannot place object that far away - max range of 5 units")
return
end
-- Prevent placement if bounding box is colliding with something
if bbox.is_colliding then
print_msg("st_collision_warning")
return
end
local obj_id = create_object(phy_obj_section, place_coordinates, bbox.rotation)
if obj_id then transfer_item_data(item_id, obj_id) end
SendScriptCallback("hf_on_furniture_place", obj_id)
place_sound:play_no_feedback(db.actor, sound_object.s3d, 0, place_coordinates, 1.0, 1.0)
bbox_drawer:Stop()
-- Reset variables
base_rot = 0
phy_obj_section = nil
-- Delete item
if item_id then
alife():release(alife():object(item_id), true)
item_id = nil
end
set_state(states.IDLE)
pr("state=PLACING")
end
elseif dik == key_toggle_collision then
if in_placing_state() then
check_collision = not check_collision
if check_collision then
print_msg("st_enable_collision")
else
print_msg("st_disable_collision")
end
end
elseif dik == key_toggle_align then
if in_placing_state() then
if align_state == align_states.ACTOR then
align_state = align_states.SURFACE
print_msg("st_align_surface")
elseif align_state == align_states.SURFACE then
align_state = align_states.ACTOR
print_msg("st_align_actor")
end
end
elseif dik == key_toggle_controls then
if state == states.IDLE then
return
elseif state == states.HOLDING or
state == states.PREVIEW then
set_state(states.ADV_CTRL)
end
open_gui()
else
local bind = dik_to_bind(dik)
if state ~= states.IDLE and not direction_keys[bind] then
pr("Interrupting placement")
set_state(states.IDLE)
bbox_drawer:Stop()
end
end
end
-- Cancel placement state with ESCAPE key
-- turns out this fires after on_key_press
local function on_before_key_press(key, bind, dis, flags)
if bind == key_bindings.kQUIT and
(is_state(states.HOLDING) or is_state(states.PREVIEW)) then
flags.ret_value = false
set_state(states.IDLE)
bbox_drawer:Stop()
end
end
------
function start_placing_item(section)
if state ~= states.IDLE then
item_id = nil
return
end
print_msg("st_place_popup")
hide_hud_inventory()
db.actor:activate_slot(0)
set_state(states.HOLDING)
local bbox_size_str = ini_sys:r_string_ex(section, "bounding_box_size", "0.5,0.5,0.5")
local bbox_size = str_explode(bbox_size_str,",")
local bbox_origin_str = ini_sys:r_string_ex(section, "bounding_box_origin", "0,0.25,0")
local bbox_origin = str_explode(bbox_origin_str,",")
base_rot = ini_sys:r_float_ex(section, "base_rotation", 0) or 0
base_loc = vector():set(tonumber(bbox_origin[1]), tonumber(bbox_origin[2]), tonumber(bbox_origin[3]))
bbox = hf_bbox.bbox_collider(tonumber(bbox_size[1]), tonumber(bbox_size[2]), tonumber(bbox_size[3]))
bbox:OffsetVertices(base_loc)
local crosshair_vertex = bbox:GetCrosshairVertex()
crosshair_vertex:sub(base_loc)
bbox:SetCrosshairVertex(crosshair_vertex)
phy_obj_section = section
end
function place_item(obj)
return gc("st_place_furniture")
end
function func_place_item(obj)
item_id = obj:id()
print_msg("st_place_popup")
physic_section = ini_sys:r_string_ex(obj:section(), "placeable_section")
if physic_section == nil then
pr("No section of physical object to place")
return
end
start_placing_item(physic_section)
end
------
-- Helper to determine if item is a placeable furniture that requires fuel
-- function is_fueled_furniture(obj)
-- local section = obj and obj:section() or false
-- if section then
-- return SYS_GetParam(0, section, "placeable_type") and SYS_GetParam(0, section, "use_condition")
-- end
-- return false
-- end
-- Monkey patch for adding fuel amount on items // DEPRECATED: using item_device binder instead, which makes these items devices
-- local clr_r = utils_xml.get_color("d_red")
-- local clr_g = utils_xml.get_color("d_green")
-- local clr_y = utils_xml.get_color("yellow")
-- local clr_2 = utils_xml.get_color("ui_gray_1")
-- original_build_desc_header = ui_item.build_desc_header
-- function ui_item.build_desc_header(obj, sec, str)
-- local _str = ""
-- local _str2 = original_build_desc_header(obj, sec, str)
-- -- display power + psu
-- if obj and is_fueled_furniture(obj) then
-- local fuel = math.ceil(obj:condition() * 100)
-- local clr = utils_xml.get_color_con(fuel)
-- _str = clr .. " " .. clr_2 .. gc("st_fuel") .. ": " .. clr .. tostring(fuel) .. "%" .. "\\n \\n" .. clr_2
-- end
-- _str = _str .. _str2
-- return _str
-- end
-------
-- UI
-------
-- Advanced Controls
---@class placeable_furniture.UIAdvancedControls
GUI = nil -- instance, don't touch
function open_gui()
hide_hud_inventory()
if (not GUI) then
GUI = UIAdvancedControls()
end
if (GUI) and (not GUI:IsShown()) then
GUI:Reset()
GUI:ShowDialog(true)
Register_UI("UIAdvancedControls","hf_advanced_controls")
end
end
class "UIAdvancedControls" (CUIScriptWnd)
function UIAdvancedControls:__init() super()
self:InitControls()
self:InitCallBacks()
self.states = {
IDLE = 1, -- idle
XZ_DRAG_LOC = 2, -- Horizontal Plane
Y_DRAG_LOC = 3, -- Up/Down
Y_DRAG_ROT = 4, -- Rotate about upward axis
}
self.state = self.states.IDLE
self.snap = false
self.shift = false
self.pos = {x=0, y=0}
self.prev_pos = nil
self.prev_offset = vector():set(location_offset)
self.init = false
end
function UIAdvancedControls:__finalize()
end
function UIAdvancedControls:KeyPress(dik, kb_action)
local funcs = self.key_to_functor[dik]
if funcs then
local func = funcs[kb_action]
if func then func() end
end
end
function UIAdvancedControls:InitControls()
self:SetWndRect(Frect():set(0,0,1024,768))
self:SetAutoDelete(true)
-- Questionable table, but i got tired of if-elses
self.key_to_functor = {
-- XZ Displacement
[DIK_keys.MOUSE_1] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
if self.shift then
self.state = self.state == self.states.IDLE and self.states.Y_DRAG_LOC or self.state
else
self.state = self.state == self.states.IDLE and self.states.XZ_DRAG_LOC or self.state
end
end,
[ui_events.WINDOW_KEY_RELEASED] = function()
self.prev_offset = vector():set(location_offset)
self.state = self.states.IDLE
self.prev_pos = nil
end
},
-- Y Displacement
[DIK_keys.DIK_LSHIFT] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
self.shift = true
end,
[ui_events.WINDOW_KEY_RELEASED] = function()
self.shift = false
end
},
-- Y Rotation
[DIK_keys.MOUSE_2] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
self.state = self.state == self.states.IDLE and self.states.Y_DRAG_ROT or self.state
end,
[ui_events.WINDOW_KEY_RELEASED] = function()
self.prev_rot = rotation_offset
self.state = self.states.IDLE
self.prev_pos = nil
end
},
-- Finer control, adjust at 1/10th speed
[DIK_keys.DIK_LCONTROL] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
self.fine_control = true
end,
[ui_events.WINDOW_KEY_RELEASED] = function()
self.fine_control = false
end
},
-- SNAP
[DIK_keys.DIK_LMENU] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
self.snap = true
end,
[ui_events.WINDOW_KEY_RELEASED] = function()
self.snap = false
end
},
-- Place
[key_place] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
on_key_press(key_place)
self:Close()
end
},
-- Toggle Collision
[key_toggle_collision] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
on_key_press(key_toggle_collision)
end
},
-- Toggle Alignment mode
[key_toggle_align] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
on_key_press(key_toggle_align)
end
},
-- Swap to PREVIEW mode
[key_toggle_controls] = {
[ui_events.WINDOW_KEY_PRESSED] = function()
set_state(states.PREVIEW)
self:Close()
end
}
}
end
function UIAdvancedControls:InitCallBacks()
end
function UIAdvancedControls:Reset()
self.state = self.states.IDLE
self.snap = false
self.pos = {x=0, y=0}
self.prev_pos = nil
self.prev_offset = vector():set(location_offset)
self.location_offset_copy = vector():set(location_offset)
self.prev_rot = rotation_offset
self.prev_rot_copy = rotation_offset
self.init = false
end
function UIAdvancedControls:Update()
CUIScriptWnd.Update(self)
if self.state == self.states.IDLE then return end
if self.prev_pos == nil then
self.prev_pos = GetCursorPosition()
end
local mouse_pos = GetCursorPosition()
local diff_x = (mouse_pos.x - self.prev_pos.x)
local diff_y = -(mouse_pos.y - self.prev_pos.y)
if self.fine_control then
diff_x = diff_x / 10
diff_y = diff_y / 10
end
if self.state == self.states.XZ_DRAG_LOC then
diff_x = diff_x / 200
diff_y = diff_y / 200
if self.snap then
if math.abs(diff_x) > math.abs(diff_y) then
diff_y = 0
else
diff_x = 0
end
end
location_offset = vector():set(self.prev_offset.x+diff_x, self.prev_offset.y, self.prev_offset.z+diff_y)
elseif self.state == self.states.Y_DRAG_LOC then
diff_y = diff_y / 200
location_offset = vector():set(self.prev_offset.x, self.prev_offset.y+diff_y, self.prev_offset.z)
elseif self.state == self.states.Y_DRAG_ROT then
diff_x = diff_x/2
rotation_offset = self.prev_rot + diff_x
if self.snap then
local n = 15
rotation_offset = (rotation_offset % n) > n/2 and rotation_offset + n - rotation_offset%n or rotation_offset - rotation_offset%n
end
end
end
function UIAdvancedControls:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if not self.init then
self.init = true
return
end
if (res == false) then
self:KeyPress(dik, keyboard_action)
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
if self.state == self.states.IDLE then
location_offset = vector():set(self.location_offset_copy)
rotation_offset = self.prev_rot_copy
set_state(states.HOLDING)
self:Close()
else
location_offset = vector():set(self.prev_offset)
rotation_offset = self.prev_rot
self.state = self.states.IDLE
end
end
end
end
return res
end
function UIAdvancedControls:Close()
self:HideDialog()
Unregister_UI("UIAdvancedControls")
end
-- Advanced Interaction Menu
---@class arm.UIRadialMenu
RadialGUI = nil
function create_interact_gui()
local RadialGUI = arm.UIRadialMenu()
-- Pickup
local opt_pickup = arm.OptionData("opt_pickup", function (state)
return arm.get_stateful_texture("ui_hf_radial_icon_pickup", state)
end)
opt_pickup:SetColour(function (state)
return arm.get_stateful_colour(state) -- stateful colour
end) -- stateful colour
-- Text
opt_pickup:SetText({title = gc("st_pickup")})
-- Open UI
local opt_ui = arm.OptionData("opt_use", function (state)
return arm.get_stateful_texture("ui_hf_radial_icon_use", state)
end)
opt_ui:SetColour(function (state)
return arm.get_stateful_colour(state) -- stateful colour
end) -- stateful colour
-- Text
opt_ui:SetText({title = gc("st_open_adv_ui")})
-- Freeze/Unfreeze
local opt_freeze = arm.OptionData("opt_freeze", function(state)
if state == arm.States.HIGHLIGHTED then
return "ui_hf_radial_icon_frozen"
else
return "ui_hf_radial_icon_unfrozen"
end
end)
opt_freeze:SetColour(function (state)
return arm.get_stateful_colour(state) -- stateful colour
end) -- stateful colour
-- Text
opt_freeze:SetText({title = gc("st_freeze")})
-- Upgrade
local opt_upgrade = arm.OptionData("opt_upgrade", "ui_hf_radial_icon_upgrade")
opt_upgrade:SetColour(function (state)
return arm.get_stateful_colour(state) -- stateful colour
end) -- stateful colour
-- Text
opt_upgrade:SetText({title = gc("st_cap_upgrades"),
description = gc("st_not_implemented")})
opt_upgrade:SetState(arm.States.DISABLED)
-- Wire Connections
local opt_connections = arm.OptionData("opt_connections", "ui_hf_radial_icon_connections")
opt_connections:SetColour(function (state)
return arm.get_stateful_colour(state) -- stateful colour
end)
-- Text
opt_connections:SetText({title = gc("st_open_connections"),
description = gc("st_not_implemented")})
opt_connections:SetState(arm.States.DISABLED)
RadialGUI:RegisterCallback("opt_pickup", function ()
local obj = get_obj_at_crosshair()
if not obj then return end
pickup_obj(obj)
end)
RadialGUI:RegisterCallback("opt_use", function ()
local wrapper = get_wrapper_at_crosshair()
if not wrapper then return end
wrapper:use_callback()
end)
RadialGUI:RegisterCallback("opt_freeze", function ()
local wrapper = get_wrapper_at_crosshair()
if not wrapper then return end
wrapper:set_frozen(not wrapper.is_frozen)
if wrapper.is_frozen then
opt_freeze:SetState(arm.States.HIGHLIGHTED)
opt_freeze:SetText({title = gc("st_unfreeze")})
else
opt_freeze:SetState(arm.States.ENABLED)
opt_freeze:SetText({title = gc("st_freeze")})
end
end)
RadialGUI:AddOption(opt_ui)
RadialGUI:AddOption(opt_pickup)
RadialGUI:AddOption(opt_freeze)
RadialGUI:AddOption(opt_upgrade)
RadialGUI:AddOption(opt_connections)
RadialGUI:DrawOptions()
return RadialGUI
end
---@param wrapper bind_hf_base.hf_binder_wrapper
function update_radial_gui(wrapper)
local is_pickupable = wrapper:is_pickupable()
if is_pickupable then
RadialGUI:GetOption("opt_pickup"):SetState(arm.States.ENABLED)
else
RadialGUI:GetOption("opt_pickup"):SetState(arm.States.DISABLED)
end
local type = hf_furniture_types.get_type(wrapper.object)
if type == "prop" or type == nil then
RadialGUI:GetOption("opt_use"):SetState(arm.States.DISABLED)
else
RadialGUI:GetOption("opt_use"):SetState(arm.States.ENABLED)
end
if wrapper.is_frozen then
RadialGUI:GetOption("opt_freeze"):SetState(arm.States.HIGHLIGHTED)
RadialGUI:GetOption("opt_freeze"):SetText({title = gc("st_unfreeze")})
else
RadialGUI:GetOption("opt_freeze"):SetState(arm.States.ENABLED)
RadialGUI:GetOption("opt_freeze"):SetText({title = gc("st_freeze")})
end
end
---@param wrapper bind_hf_base.hf_binder_wrapper
function open_interact_gui(wrapper)
hide_hud_inventory()
if (not RadialGUI) then
RadialGUI = create_interact_gui()
end
if (RadialGUI) and (not RadialGUI:IsShown()) then
update_radial_gui(wrapper)
RadialGUI:ShowDialog(true)
_GUIs_keyfree["UIRadialMenu"] = true
Register_UI("UIRadialMenu","placeable_furniture")
end
end
-------
function on_option_change(mcm) --new in mcm 1.6.0 mcm passes true to the on_option_change callback
if mcm then
key_toggle_collision = ui_mcm.get("aol_hf/controls/bind_collision") or key_toggle_collision
key_toggle_align = ui_mcm.get("aol_hf/controls/bind_alignment") or key_toggle_align
key_toggle_controls = ui_mcm.get("aol_hf/controls/bind_place_mode") or key_toggle_controls
end
key_place = bind_to_dik(key_bindings.kUSE)
end
-- Allow drag n drop of fuel onto furniture items
local function on_item_drag_dropped(obj_b, obj_d, slot_from, slot_to)
-- Check capability
if not (slot_from == EDDListType.iActorBag and (slot_to == EDDListType.iActorBag or slot_to == EDDListType.iActorSlot)) then
return
end
local sec_b = obj_b:section() -- fuel
local sec_d = obj_d:section() -- light source
local req_fuel = ini_sys:r_string_ex(sec_d, "fuel_section")
if req_fuel and (sec_b == req_fuel) then
if sec_b == "batteries_dead" then
alife_create_item("batteries_dead", db.actor, {cond=obj_d:condition()})
obj_d:set_condition(obj_b:condition())
else
utils_item.discharge(obj_b, 1)
obj_d:set_condition(1.0)
end
utils_obj.play_sound("interface\\items\\inv_items_generic_1")
end
end
---@param obj game_object
function pickup_obj(obj)
local wrapper = bind_hf_base.get_wrapper(obj:id())
if not wrapper then return end
if wrapper:is_pickupable() then
wrapper:pickup()
print_msg("Picked up object")
end
end
-------
function on_game_start()
RegisterScriptCallback("on_option_change",on_option_change)
on_option_change(mcm_keybinds)
RegisterScriptCallback("on_key_press", on_key_press)
RegisterScriptCallback("on_key_hold", on_key_hold)
RegisterScriptCallback("on_before_key_press", on_before_key_press)
RegisterScriptCallback("actor_on_update", actor_on_update)
RegisterScriptCallback("ActorMenu_on_item_drag_drop", on_item_drag_dropped)
end
@@ -0,0 +1,57 @@
---@param npc game_object
local function generate_trade_table(npc)
local ini_trades = ini_file("items\\settings\\hideout_furniture\\trade\\trades.ltx")
local trade_table = {}
local npc_faction = trader_autoinject.get_real_community(npc, "stalker")
local npc_supply_level = trader_autoinject.supply_level(npc, true) or 1
local ignore_faction_restrictions = ui_mcm.get("aol_hf/gameplay/uniform_trade_profiles") or false
ini_trades:section_for_each(function(section)
-- Check if NPC meets required supply level
local min_supply_level = ini_trades:r_float_ex(section, "min_supply_level", 1)
if min_supply_level > npc_supply_level then return end
-- Check if NPC is in a valid faction
local factions = ini_trades:r_list(section, "factions", "all")
local is_faction_valid = false
if ignore_faction_restrictions then
is_faction_valid = true
else
for _, faction in pairs(factions) do
if faction == npc_faction or faction == "all" then
is_faction_valid = true
break
end
end
end
if not is_faction_valid then return end
-- Add items to trade table
local items = ini_trades:r_list(section, "items")
for _, item in pairs(items) do
local amount = ini_trades:r_float(section, "amount") or 1
trade_table[item] = amount
end
end)
return trade_table
end
function spawn_items(npc)
local is_supplier = trader_autoinject.get_trader_type(npc) == trader_autoinject.SUPPLIER
if not is_supplier then return end
local trade_table = generate_trade_table(npc)
if not trade_table then return end
trader_autoinject.spawn_items(npc, trade_table, true)
end
TraderAuto = trader_autoinject.update
function trader_autoinject.update(npc)
TraderAuto(npc)
spawn_items(npc)
end
@@ -0,0 +1,617 @@
--[[
Tronex
Last edit: 2018/5/22
PDA radio/music player
==============================================================================================
Codes/comments with ( --< can be expanded for more channels >-- ) tag can be edited to add more radio channels
Creating new radio channel require editing the other XML and DDS files as well.
Using (radio_vol = soundObject.volume) will cause a crash if you didn't give the object a volume value
Sounds start always at maxed volume before going down if you declare a volume value before playing the sound
Don't use (play_no_feedback) method, it has no controls
volume values changes give fractions (for example 0.2 change is actually 0.20000003278255)
Codes/comments with ( --< can be expanded for more channels >-- ) tag can be edited to add more radio channels
==============================================================================================
--]]
------------------------------------------------------------
-- Controls
------------------------------------------------------------
-- Files links & controls
local ini_radio = ini_file("plugins\\radio_zone_fm.ltx") -- File control: reading the ltx file that controls the hotkeys
local num_of_ch = 0 --< can be expanded for more channels >--- Number of Radio channels
local num_of_noise = 8 -- File control: number of files with name (white_noise_heavy_horror_)
local num_of_mixdown = 4 -- File control: number of files with name (radio_mixdown)
local path_radio_ch = {} -- File control: the path to channel x tracks
local path_radio_session = {} -- File control: the path to channel x sessions
local radio_ch_names = {}
local radio_frequencies = {}
local ini_channels = ini_file_ex("plugins\\placeable_radio\\base.ltx")
local i = 1
for _, section in pairs(ini_channels:get_sections()) do
path_radio_ch[i] = ini_channels:r_string_ex(section, "program")
path_radio_session[i] = ini_channels:r_string_ex(section, "intermission")
radio_ch_names[i] = ini_channels:r_string_ex(section, "name")
radio_frequencies[i] = ini_channels:r_string_ex(section, "frequency")
num_of_ch = num_of_ch + 1
i = i + 1
end
local fileList, count, file -- Variables for file reading
-- Volume controls
local radio_vol = 0.5 -- In-game control: Radio volume - first value
local vol_max = 0.98 -- In-game control: Max volume value (1.1 - anything beyond 1.0 has no additional effect)
local vol_min = 0.12 -- In-game control: Min volume value (0.1 - still hearable)
local vol_step = 0.2 -- In-game control: Volume change per click
local psi_vol = 0 -- This value get subtracted from the main volume value, it become >0 on emissions to reduce the total radio volume
-- Others
local safety_lock = false -- become "true" after making change. while true, it prevents any possible additional changes done by user for a brief amount of time until the code is over. basically a safety procedure to prevent codes from interfering with each others (locked = true, free to go = false)
local white_noise_now = 1 -- Current index of the white noise
local psi_start_lock = false -- used to separate stages of for emission noise effects
local psi_time = time_global() or 0 -- period between radio volume shifts for emission/underground noise effects
local indoor_lvls = {} -- Table: contains names of underground levels
local gini = game_ini()
local levels = utils_data.collect_section(gini,"level_maps_single")
for i=1,#levels do
wthr = gini:r_string_ex(levels[i],"weathers")
if (wthr == "indoor") then
indoor_lvls[levels[i]] = true
end
end
-- Hotkeys
local opt = {}
function update_settings()
opt.emission_noise = ui_options.get("sound/radio/emission_intereferences")
opt.underground_noise = ui_options.get("sound/radio/underground_intereferences")
opt.display_names = ui_options.get("sound/radio/display_tracks")
end
------------------------------------------------------------
-- Binder
------------------------------------------------------------
function init(obj)
obj:bind_object(placeable_radio_wrapper(obj).binder)
end
--------------------------------------------------------------------------------
-- Class "placeable_radio_wrapper"
--------------------------------------------------------------------------------
class "placeable_radio_wrapper" (bind_hf_base.hf_binder_wrapper)
-- Class constructor
function placeable_radio_wrapper:__init(obj) super(obj)
-- Radio
self.radio_ch = {} -- Multi-dimensional table: Radio channels, every channel has its tracks
self.radio_ch_out = {} -- Similar, used for outer radios
self.radio_index = {} -- Multi-dimensional table: Radio channels indexes, every channel index has its track indexes
self.radio_now = {} -- Table: to determine the current track for each channel
for x = 1, num_of_ch do -- Reading the files for radio channels
self.radio_ch[x] = {}
self.radio_ch_out[x] = {}
self.radio_index[x] = {}
self.radio_now[x] = 1
fileList = getFS():file_list_open("$game_sounds$", path_radio_ch[x], bit_or(FS.FS_ListFiles, FS.FS_RootOnly)) -- Reading the file list in channel x
count = fileList and fileList:Size() or 0 -- Get the number of files per channel x
if (count > 0) then
for i = 1, count do
file = fileList:GetAt(i - 1):sub(1, -5) -- Get red off the extension
self.radio_ch[x][i] = sound_object(path_radio_ch[x] .. file) -- Creating sound objects, every track in every channel
self.radio_ch_out[x][i] = sound_object(path_radio_ch[x] .. file)
self.radio_index[x][i] = i -- Creating track index
end
else -- if no files found
self.radio_ch[x][1] = sound_object("radio\\no_sound") -- assign no sound, a small hack to prevent the game from crashing
self.radio_ch_out[x][1] = sound_object("radio\\no_sound")
self.radio_index[x][1] = 1
end
end
self.radio_state = false -- Radio on/off state (off = false, on = true)
self.radio_selected = 1 -- Selected Radio channel (0 means nothing is selected)
self.radio_hotkey_switch = false -- Safety switch for (Play/Stop Radio) Hotkey
-- Radio sessions
self.radio_session = {} -- Multi-dimensional table: Radio channels commercial sessions, every channel has its sessions
self.radio_session_out = {} -- Similar, used for outer radios
self.radio_session_now = {} -- Current session per channel
self.radio_session_lock = {} -- Boolean table: used to make sure that only 1 session is being played between tracks
for x = 1, num_of_ch do -- assign sound files for sessions
self.radio_session[x] = {}
self.radio_session_out[x] = {}
self.radio_session_now[x] = 1
self.radio_session_lock[x] = false
fileList = getFS():file_list_open("$game_sounds$", path_radio_session[x], bit_or(FS.FS_ListFiles, FS.FS_RootOnly)) -- Reading the file list in channel x sessions
count = fileList and fileList:Size() or 0 -- Get the number of files per channel x
if (count > 0) then
for i = 1, count do
file = fileList:GetAt(i - 1):sub(1, -5)
self.radio_session[x][i] = sound_object(path_radio_session[x] .. file)
self.radio_session_out[x][i] = sound_object(path_radio_session[x] .. file)
end
else
self.radio_session[x][1] = sound_object("radio\\no_sound")
self.radio_session_out[x][1] = sound_object("radio\\no_sound")
end
end
-- Sound effects
self.snd_radio_mixdown = {}
self.snd_white_noise_heavy_horror = {}
self.snd_radio_on = sound_object("radio\\interact\\radio_on")
self.snd_radio_off = sound_object("radio\\interact\\radio_off")
self.snd_white_noise_light = sound_object("radio\\white_noise\\white_noise_light")
self.snd_white_noise_heavy = sound_object("radio\\white_noise\\white_noise_heavy")
self.snd_white_noise_heavy_start = sound_object("radio\\white_noise\\white_noise_heavy_fade_in")
self.snd_white_noise_heavy_end = sound_object("radio\\white_noise\\white_noise_heavy_fade_out")
for i = 1, num_of_mixdown do
self.snd_radio_mixdown[i] = sound_object("radio\\interact\\radio_mixdown_" .. tostring(i))
self.snd_radio_mixdown[i].volume = 2
end
for i = 1, num_of_noise do
self.snd_white_noise_heavy_horror[i] = sound_object("radio\\white_noise\\White_noise_heavy_horror_" .. tostring(i))
end
self.snd_radio_on.volume = 2
self.snd_radio_off.volume = 2
self.object:set_tip_text(game.translate_string("st_interact"))
end
-- Class update
function placeable_radio_wrapper:update(delta)
-- self.object:set_callback(callback.use_object, placeable_radio_wrapper.use_callback, self)
if not self.init then
local phys = self.object:get_physics_shell()
if phys then
phys:apply_force(0, 1, 0)
self.init = true
end
end
-- Radio (no channel) part
if self.radio_state and (self.radio_selected == 0) then
if not self.snd_white_noise_light:playing() then
self.snd_white_noise_light:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
self.snd_white_noise_light.volume = 0.4
end
else
self.snd_white_noise_light:stop()
end
-- Consume power from PDA if radio/player is ON
-- change obj:condition()
if (not safety_lock) then
-- Radio part
if (not self:is_snd_playing()) then
for i=1,num_of_ch do
if (not self.radio_ch[i][self.radio_now[i]]:playing()) and (not self.radio_session[i][self.radio_session_now[i]]:playing()) then -- if no track and no session is playing in channel (i)
if (math.random(3) == 1) and (not self.radio_session_lock[i]) then -- chance of playing a session between tracks (%30)
self.radio_session_now[i] = math.random(#self.radio_session[i]) -- pick a session from channel i
self.radio_session[i][self.radio_session_now[i]]:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d) -- play it
self.radio_session_lock[i] = true
else -- if no session is picked, go for radio tracks
self.radio_now[i] = self:radio_pick (self.radio_index[i], #self.radio_ch[i]) -- pick a track from channel i
self.radio_ch[i][self.radio_now[i]]:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d) -- play it
self.radio_session_lock[i] = false
end
end
if self.radio_state and (self.radio_selected == i) then
self.radio_ch[i][self.radio_now[i]].volume = radio_vol - psi_vol
self.radio_session[i][self.radio_session_now[i]].volume = radio_vol - psi_vol
else
self.radio_ch[i][self.radio_now[i]].volume = 0
self.radio_session[i][self.radio_session_now[i]].volume = 0
end
end
end
end
-- Emission part
if opt.emission_noise then
if xr_conditions.surge_started() or psi_storm_manager.is_started() then
if not psi_start_lock then -- lock to make sure this condition happen once per emission
psi_start_lock = true
self.snd_white_noise_heavy_start:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
end
if (not self.snd_white_noise_heavy_start:playing()) and (not self.snd_white_noise_heavy_horror[white_noise_now]:playing()) then
white_noise_now = math.random(#self.snd_white_noise_heavy_horror)
self.snd_white_noise_heavy_horror[white_noise_now]:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
end
if (math.floor(psi_time + 1500) < time_global()) then
psi_time = time_global()
psi_vol = math.random(2,8)/10
end
elseif psi_start_lock then
if (xr_conditions.surge_complete() or psi_storm_manager.is_finished()) and (not self.snd_white_noise_heavy_horror[white_noise_now]:playing()) and (not self.snd_white_noise_heavy_start:playing()) and (not self.snd_white_noise_heavy_end:playing()) then -- if emission ended AND start/end sound are not playing AND emission lock is on
self.snd_white_noise_heavy_end:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
psi_start_lock = false
psi_vol = 0
end
end
else
self.snd_white_noise_heavy_horror[white_noise_now]:stop()
self.snd_white_noise_heavy_start:stop()
self.snd_white_noise_heavy_end:stop()
psi_start_lock = false
psi_vol = 0
end
-- Underground part
if indoor_lvls[level.name()] then
if opt.underground_noise then
if (not self.snd_white_noise_heavy:playing()) then
self.snd_white_noise_heavy:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
end
if (math.floor(psi_time + 1500) < time_global()) then
psi_time = time_global()
psi_vol = math.random(5,8)/10
end
else
psi_vol = 0
end
end
if self.radio_state then
self.snd_white_noise_heavy_horror[white_noise_now].volume = radio_vol
self.snd_white_noise_heavy_start.volume = radio_vol
self.snd_white_noise_heavy_end.volume = radio_vol
self.snd_white_noise_heavy.volume = radio_vol
else
self.snd_white_noise_heavy_horror[white_noise_now].volume = 0
self.snd_white_noise_heavy_start.volume = 0
self.snd_white_noise_heavy_end.volume = 0
self.snd_white_noise_heavy.volume = 0
end
end
function placeable_radio_wrapper:use_callback()
hide_hud_inventory()
start_radio_dialog(self.object:id())
end
function placeable_radio_wrapper:use_callback_simple()
if self.radio_state then
self:action_radio_stop()
else
self:action_radio_start()
end
end
------------------------------------------------------------
-- Functionality
------------------------------------------------------------
function placeable_radio_wrapper:action_radio_ch(n) -- Rule: Switch to Channel (n)
if self.radio_state and (not safety_lock) and (not self:is_snd_playing()) and (self.radio_selected ~= n) then
safety_lock = true
-- get_ui():SwitchChannel(n)
self:radio_setVolume(0) -- set volume 0 to the previous channel
self.radio_selected = n
--snd_click:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
self.snd_radio_mixdown[math.random(#self.snd_radio_mixdown)]:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
safety_lock = false
return true
end
return false
end
function placeable_radio_wrapper:action_radio_v_down() -- Rule: Reduce the radio volume
if (radio_vol > vol_min) and self.radio_state and (not safety_lock) and (not self:is_snd_playing()) then
safety_lock = true
--snd_click:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
radio_vol = radio_vol - vol_step
self:radio_setVolume(radio_vol - psi_vol)
safety_lock = false
end
end
function placeable_radio_wrapper:action_radio_v_up() -- Rule: Increase the radio volume
if (radio_vol < vol_max) and self.radio_state and (not safety_lock) and (not self:is_snd_playing()) then
safety_lock = true
--snd_click:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
radio_vol = radio_vol + vol_step
self:radio_setVolume(radio_vol - psi_vol)
safety_lock = false
end
end
function placeable_radio_wrapper:action_radio_stop() -- Rule: Stop the radio
if self.radio_state and (not safety_lock) and (not self:is_snd_playing()) then
safety_lock = true
self.radio_state = false
--snd_click:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
self.snd_radio_off:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
self:radio_setVolume(0)
safety_lock = false
end
end
function placeable_radio_wrapper:action_radio_start() -- Rule: Start the radio
if (not self.radio_state)
and (not safety_lock)
and (not self:is_snd_playing())
then
safety_lock = true
self.radio_state = true
--snd_click:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
self.snd_radio_on:play_at_pos(self.object, self.object:position(), 0, sound_object.s3d)
self:radio_setVolume(radio_vol - psi_vol)
safety_lock = false
end
end
----------------------------------------------------------------------------------------
function placeable_radio_wrapper:is_snd_playing() -- Rule: check if some interacting sound is currently playing | it prevents any changes done by the player until that sound is over (no sound interfering)
if self.snd_radio_on:playing() or self.snd_radio_off:playing() then
return true
end
for j = 1, #self.snd_radio_mixdown do
if self.snd_radio_mixdown[j]:playing() then
return true
end
end
return false
end
function placeable_radio_wrapper:radio_setVolume(radio_vol) -- Rule: set volume for selected channel
-- This might seem unnecessary at first glance since the volume get modified always through (actor_on_update) callback
-- but tests showed that volume changes slowly, not instantly.
-- with bigger code snippets between the sound play and volume change, its even slower.
-- This function make things faster for volume changes.
if (self.radio_selected ~= 0) then
self.radio_ch[self.radio_selected][self.radio_now[self.radio_selected]].volume = radio_vol
self.radio_session[self.radio_selected][self.radio_session_now[self.radio_selected]].volume = radio_vol
end
end
function placeable_radio_wrapper:radio_pick(radio_index_i, number_of_tracks) -- Rule: pick a random index for radio channels, get rid of the picked index afterwards so it doesn't get chosen again until a new cycle is called
if (#radio_index_i == 0) then
for i=1,number_of_tracks do
radio_index_i[i] = i
end
end
local j = math.random(#radio_index_i)
local k = radio_index_i[j]
table.remove(radio_index_i, j)
return k
end
function placeable_radio_wrapper:get_playing_object(i) -- called from ph_sound to play current track on the radio on radio objects
local sound_obj = self.radio_session_out[i][self.radio_session_now[i]]
if (self.radio_session[i][self.radio_session_now[i]]:playing()) then
return sound_obj
end
sound_obj = self.radio_ch_out[i][self.radio_now[i]]
if (self.radio_ch[i][self.radio_now[i]]:playing()) then
return sound_obj
end
return
end
------------------------------------------------------------
-- GUI
------------------------------------------------------------
local SINGLETON = nil
function start_radio_dialog(obj_id) -- get called from pressing the tab
SINGLETON = SINGLETON or UIFurnitureRadio()
SINGLETON:TestAndShow(obj_id)
end
class "UIFurnitureRadio" (CUIScriptWnd)
function UIFurnitureRadio:__init() super()
self.snd_click = sound_object("radio\\interact\\click")
self.radio_display_ch = {}
-- for i=1,num_of_ch do
-- self.radio_display_ch[i] = false -- If true, it will display channel i red marker
-- end
self.frequency_min = 88
self.frequency_max = 110
self.frequency_min_x = 14
self.frequency_max_x = 397
self.frequency = 88
self:InitControls()
self:InitCallbacks()
end
function UIFurnitureRadio:__finalize()
SINGLETON = nil
end
function UIFurnitureRadio:TestAndShow(obj_id)
self.obj_id = obj_id
self:ShowDialog(true)
Register_UI("UIFurnitureRadio","ui_radio_dialog")
end
function UIFurnitureRadio:Pickup()
local obj = get_object_by_id(self.obj_id)
local item_section = ini_sys:r_string_ex(obj:section(), "item_section") or "device_gas_lamp"
-- local condition = obj:binded_object().fuel
alife_create_item(item_section, db.actor)
-- local item = alife():create(item_section, db.actor:position(), 1, db.actor:game_vertex_id(), db.actor:id())
-- get_object_by_id(item.id):set_condition(obj:binded_object().fuel)
alife_release(obj)
self:Close()
end
function UIFurnitureRadio:InitControls() -- Rule: Prepare the UI
local xml = CScriptXmlInit()
xml:ParseFile("ui_furniture_radio.xml")
-- Main frame
self:SetWndRect(Frect():set(0, 0, 1024, 768))
xml:InitFrame("background", self)
self.btn_pickup = xml:Init3tButton("btn_pickup", self)
self:Register(self.btn_pickup, "btn_pickup")
self.btn_close = xml:Init3tButton("btn_close", self)
self:Register(self.btn_close, "btn_close")
-- Radio channels
self.frequency_bg = xml:InitStatic("interface_radio_bg", self)
self.frequency_needle = xml:InitStatic("interface_radio_needle", self.frequency_bg)
self.btn_radio_ch = {}
self.list_channel = xml:InitComboBox("list_channel", self)
self:Register(self.list_channel, "list_channel")
for i=1,num_of_ch do
self.list_channel:AddItem(game.translate_string(radio_ch_names[i]), i)
end
local index = prevChannel or 1
self.list_channel:SetText(game.translate_string(radio_ch_names[index]))
-- Radio buttons
self.btn_radio_v_down = xml:Init3tButton("btn_radio_v_down", self)
self:Register(self.btn_radio_v_down, "btn_radio_v_down")
self.btn_radio_v_up = xml:Init3tButton("btn_radio_v_up", self)
self:Register(self.btn_radio_v_up, "btn_radio_v_up")
self.btn_radio_stop = xml:Init3tButton("btn_radio_stop", self)
self:Register(self.btn_radio_stop, "btn_radio_stop")
self.btn_radio_start = xml:Init3tButton("btn_radio_start", self)
self:Register(self.btn_radio_start, "btn_radio_start")
end
function UIFurnitureRadio:InitCallbacks()
-- Radio
-- for i=1,num_of_ch do
-- self:AddCallback("btn_radio_ch_" .. i, ui_events.BUTTON_CLICKED, self["On_Radio_Channel_" .. i], self)
-- end
self:AddCallback("btn_close", ui_events.BUTTON_CLICKED, self.Close, self)
self:AddCallback("btn_pickup", ui_events.BUTTON_CLICKED, self.Pickup, self)
self:AddCallback("list_channel", ui_events.LIST_ITEM_SELECT, self.OnSelectChannel, self)
self:AddCallback("btn_radio_stop", ui_events.BUTTON_CLICKED, self.On_Radio_Stop, self)
self:AddCallback("btn_radio_start", ui_events.BUTTON_CLICKED, self.On_Radio_Start, self)
self:AddCallback("btn_radio_v_down", ui_events.BUTTON_CLICKED, self.On_Radio_Volume_Down, self)
self:AddCallback("btn_radio_v_up", ui_events.BUTTON_CLICKED, self.On_Radio_Volume_Up, self)
end
function UIFurnitureRadio:Update() -- Rule: called automatically to update the PDA UI
CUIScriptWnd.Update(self)
local pos = vector2():set(0, 0)
local range_freq = self.frequency_max - self.frequency_min
local percent = (self.frequency - self.frequency_min) / range_freq
local ui_range = self.frequency_max_x - self.frequency_min_x
pos.x = self.frequency_min_x + (ui_range * percent) - (self.frequency_needle:GetWidth()/2)
self.frequency_needle:SetWndPos(pos)
end
function UIFurnitureRadio:SwitchChannel(freq)
self.frequency = freq
end
function UIFurnitureRadio:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if (res == false) then
local bind = dik_to_bind(dik)
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
self:Close()
end
end
end
return res
end
function UIFurnitureRadio:Close()
if (self:IsShown()) then
self:HideDialog()
end
--self.object:give_info_portion("tutorial_sleep")
Unregister_UI("UIFurnitureRadio")
end
-- Callbacks
function UIFurnitureRadio:OnSelectChannel()
local channel_id = self.list_channel:CurrentID()
local wrapper = bind_hf_base.get_wrapper(self.obj_id)
local result = wrapper:action_radio_ch(channel_id)
if result then
self:SwitchChannel(radio_frequencies[channel_id])
self.snd_click:play(db.actor, 0, sound_object.s2d)
end
end
function UIFurnitureRadio:On_Radio_Channel_1()
local wrapper = bind_hf_base.get_wrapper(self.obj_id)
local result = wrapper:action_radio_ch(1)
if result then
self:SwitchChannel(1)
self.snd_click:play(db.actor, 0, sound_object.s2d)
end
end
function UIFurnitureRadio:On_Radio_Channel_2()
local wrapper = bind_hf_base.get_wrapper(self.obj_id)
local result = wrapper:action_radio_ch(2)
if result then
self:SwitchChannel(2)
self.snd_click:play(db.actor, 0, sound_object.s2d)
end
end
--< can be expanded for more channels >-- you need to make new function for each new channel
function UIFurnitureRadio:On_Radio_Volume_Down()
local wrapper = bind_hf_base.get_wrapper(self.obj_id)
wrapper:action_radio_v_down()
self.snd_click:play(db.actor, 0, sound_object.s2d)
end
function UIFurnitureRadio:On_Radio_Volume_Up()
local wrapper = bind_hf_base.get_wrapper(self.obj_id)
wrapper:action_radio_v_up()
self.snd_click:play(db.actor, 0, sound_object.s2d)
end
function UIFurnitureRadio:On_Radio_Stop()
local wrapper = bind_hf_base.get_wrapper(self.obj_id)
wrapper:action_radio_stop()
self.snd_click:play(db.actor, 0, sound_object.s2d)
end
function UIFurnitureRadio:On_Radio_Start()
local wrapper = bind_hf_base.get_wrapper(self.obj_id)
wrapper:action_radio_start()
self.snd_click:play(db.actor, 0, sound_object.s2d)
end
function on_game_start()
local function on_game_load()
update_settings()
end
RegisterScriptCallback("on_option_change",update_settings)
RegisterScriptCallback("on_game_load",on_game_load)
end
@@ -0,0 +1,98 @@
-- Track stashes with capacities
local stash_sections = {["inv_backpack"] = true, ["inventory_box"] = true}
local function itr_for_stashes(section)
local placeable_type = ini_sys:r_string_ex(section, "placeable_type") or "prop"
if placeable_type == "stash" then
stash_sections[section] = true
end
end
ini_sys:section_for_each(itr_for_stashes)
local default_capacity = 30
local weight_add = nil -- used when moving items into a stash
local function getInvWeight(stash_id)
local stash = get_object_by_id(stash_id)
local weight = 0
stash:iterate_inventory_box( function(owner,itm)
weight = weight + itm:weight()
end)
return weight
end
-- Update Max Capacity of stash inventories
inventory_update = ui_inventory.UIInventory.UpdateWeight
function ui_inventory.UIInventory:UpdateWeight()
inventory_update(self)
local stash = self:GetPartner()
if not stash then return end
local section = stash:section()
if not stash_sections[section] then return end
local capacity = ini_sys:r_float_ex(section,"capacity") or default_capacity
self.npc_weight_max:SetText( strformat("(max %s %s)", capacity, game.translate_string("st_kg")) )
end
-- Make Max Capacity text visible for stashes
inventory_reset = ui_inventory.UIInventory.Reset
function ui_inventory.UIInventory:Reset(obj)
inventory_reset(self, obj)
if self:GetPartner() then
self.npc_weight_max:Show( (self.npc_is_companion and self.mode == "loot") or stash_sections[self:GetPartner():section()])
end
end
put_all = ui_inventory.UIInventory.LMode_PutAll
function ui_inventory.UIInventory:LMode_PutAll()
weight_add = 0
put_all(self)
weight_add = nil
end
function ui_inventory.UIInventory:Action_Move_All(obj, bag)
weight_add = 0
obj = self:CheckItem(obj,"Action_Move_All")
local ci = self.CC[bag]:GetCell_ID(obj:id())
for id,_ in pairs(ci.childs) do
if self:Cond_Move(id, bag) then
self:Action_Move(id, bag)
end
end
if self:Cond_Move(obj, bag) then
self:Action_Move(obj, bag)
end
weight_add = nil
end
local function actor_on_item_before_move(flags, npc_id, obj, mode, bag_from)
if (bag_from ~= EDDListType.iActorBag and bag_from ~= EDDListType.iActorSlot) then return end
if not obj then return end
if not npc_id then return end
local npc = get_object_by_id(npc_id)
if not npc then return end
local section = npc:section()
if not stash_sections[section] then return end
local capacity = ini_sys:r_float_ex(section,"capacity") or default_capacity
local weight = getInvWeight(npc_id) + (weight_add or 0)
if weight + obj:weight() > capacity then
flags.ret_value = false
end
end
local function actor_on_item_after_move(npc_id, obj, mode, bag_from)
if weight_add == nil then return end
weight_add = weight_add + obj:weight()
end
function on_game_start()
RegisterScriptCallback("ActorMenu_on_item_before_move", actor_on_item_before_move)
RegisterScriptCallback("ActorMenu_on_item_after_move", actor_on_item_after_move)
end
@@ -0,0 +1,148 @@
--[[
item pickup animation script
Author: Feel_Fried
--]]
local skin_anim, harv_usage_snd,enable_take
local ltx = ini_file("items\\items\\anims_loot_list.ltx")
function on_game_start()
RegisterScriptCallback("actor_on_first_update",actor_on_first_update)
RegisterScriptCallback("on_option_change", loadsettings)
end
function actor_on_first_update()
RegisterScriptCallback("actor_on_item_take",actor_on_item_take)
-- RegisterScriptCallback("actor_on_item_before_pickup",actor_on_item_before_pickup)
loadsettings()
end
function loadsettings()
if ui_mcm then
enable_animations = ui_mcm.get("EA_settings/enable_animations")
enable_take = ui_mcm.get("EA_settings/take_item_anim")
else
enable_animations = ui_options.get("video/player/animations")
enable_take = true
end
enable_multipickup = ui_options.get("control/general/pickup_mode")
end
local anim_plays = nil
local anm_name = "camera_effects\\weapon\\two_handed_weapon_effect.anm"
function start_pickup_delay(item)
if not anim_plays and not enhanced_animations.used_item then
game.play_hud_motion(db.actor:active_slot()==0 and 2 or 1, "item_ea_take_hud", "anm_ea_take", true, 1.4)
game.play_hud_anm(anm_name, 0, 1.4, 1, false)
CreateTimeEvent("ea_item_take","take_delay",0.65,take_delay,item:id())
CreateTimeEvent("ea_item_take","anim_delay",1.33,anim_delay)
anim_plays = true
end
end
local base_before_pickup = bind_stalker_ext.actor_on_item_before_pickup
function is_item_pickupable(item)
local result = base_before_pickup(item)
if result == false then return false end
return true
end
function bind_stalker_ext.actor_on_item_before_pickup(item)
local is_pickupable = is_item_pickupable(item)
if not is_pickupable then return false end -- respect pickupability as set by callback
if (not enable_take) or (not enable_animations) then return true end --1 - 10 single press, 50+ - long press
start_pickup_delay(item)
return false
end
function anim_delay()
anim_plays = nil
return true
end
function take_delay(object_id)
local obj_o = level.object_by_id(object_id)
if not obj_o then return true end
local actor = db.actor
actor:take_item(obj_o)
if enable_multipickup then
multipickup(1.5,0.75,actor)
multipickup(1,0.5,actor)
multipickup(0.5,0.25,actor)
end
printf(obj_o:section() .. " taken with id " .. obj_o:id())
actor = nil
return true
end
function multipickup(dist,radius,actor)
local pos = device().cam_pos
local dir = device().cam_dir
pos = pos:add(dir:mul(dist))
level.iterate_nearest(pos,dist,function(obj)
if obj:cast_InventoryItem()
and (pos:distance_to(obj:position()) <= radius)
and is_item_pickupable(obj) then
actor:take_item(obj)
printf(obj:section() .. " taken with id " .. obj:id())
end
end)
pos,dir = nil
end
function actor_on_item_take(item)
--mutant loot bag animation
if enhanced_animations.fake_monster then
fov_anim_manager.change_fov(0.45)
enhanced_animations.fake_monster = nil
level.disable_input()
local sect = item:section()
local item_sect_index = ltx:section_exist(sect) and ltx:r_string_ex(sect, "indx"):match("(.+),(.+)") or math.random(14)
local anim_section_name = "item_ea_harv_meat_"..item_sect_index.."_hud"
harv_usage_snd = sound_object("interface\\item_usage\\harvest_use_success")
harv_usage_snd:play(db.actor, 0, sound_object.s2d)
game.play_hud_motion(2, anim_section_name, "anm_ea_show", true, 0.75)
level.add_cam_effector("itemuse_anm_effects\\harvest_loot.anm", 8560, false, "")
CreateTimeEvent("restore_knife", "restor_knife", 1.8 , restore_knife)
skin_anim = true
item_sect_index ,anim_section_name ,sect = nil
end
end
--patching mutant loot dialog close call
function ui_mutant_loot.UIMutantLoot:Close()
--deleting fake mutant object to prevent bag animation if mutant was empty
CreateTimeEvent("delete_fake", "delete_fake", 0.2, take_item_anim.delete_fake)
self:SetMutantState()
self:HideDialog()
Unregister_UI("UIMutantLoot")
end
function delete_fake()
enhanced_animations.fake_monster = nil
--draw knife if mutant was empty or player just closed dialog without looting anything
if not skin_anim then
return_slots()
end
return true
end
function restore_knife()
return_slots()
skin_anim = nil
level.enable_input()
return true
end
function return_slots()
fov_anim_manager.restore_fov()
local slot = enhanced_animations.active_slot
db.actor:activate_slot(slot or 1)
local device = enhanced_animations.det_active
if device then device:switch_state(1) end
device = nil
slot = nil
end
@@ -0,0 +1,204 @@
--[[
Wrapper class to let you autoinject things via monkey patch to all traders, respecting the restock time.
How to use: Monkey patch the update function here in your script.
ex:
TraderAuto = trader_autoinject.update
function trader_autoinject.update(npc)
TraderAuto(npc)
add_custom_crap(npc) -- you define this function ok
end
Some functions provided below for convenience.
Note: If you want to iterate NPC inventory to check for items, fire a time event to allow the items to register on new game.
--]] --
find = string.find
local function t2c(t)
if not t then return nil end
local ct = game.CTime()
ct:set(t.Y,t.M,t.D,t.h,t.m,t.s,t.ms)
return ct
end
local function c2t(ct)
if not ct then return nil end
-- printf('%s, %s',ct,type(ct))
local Y, M, D, h, m, s, ms = 0, 0, 0, 0, 0, 0, 0
Y, M, D, h, m, s, ms = ct:get(Y, M, D, h, m, s, ms)
return { Y=Y, M=M, D=D, h=h, m=m, s=s, ms=ms }
end
local furniture = {
["esc_m_trader"] = true,
["red_m_lesnik"] = true
}
local blacklisted_comms = {
["trader"] = true,
["monster"] = true
}
TraderUpdate = trade_manager.update
function trade_manager.update(npc, force_refresh)
local id = npc:id()
if not npc or not npc:alive() then
return
end
-- sid/forester fix - check if close by and then run
if furniture[npc:name()] and npc:position():distance_to(db.actor:position()) > 20 then return end
local reup_time = trade_manager.get_trade_profile(id, "resupply_time")
TraderUpdate(npc, force_refresh)
local restock_time = game_difficulties.get_eco_factor("restock") or 24
if force_refresh then restock_time = 0 end
if reup_time and game.get_game_time():diffSec(t2c(reup_time)) < (restock_time * 3600) then
-- print_dbg("Not time to resupply yet!")
return
end
disable_info("sleep_active")
CreateTimeEvent("custom_update"..npc:id(), "custom_resupply"..npc:id(), 0.1, timed_update, npc)
end
-- Add easier to trace callback
function timed_update(npc)
update(npc)
SendScriptCallback("trader_on_restock",npc)
return true
end
-- monkeypatch me
function update(npc)
end
-- util functions to help with monkey patching
function get_faction_goodwill(faction)
end
COMPANION = 0 -- companions got special trade logic, this is just to catch errors
MECHANIC = 1 -- mechanics/techs
BARMAN = 2 -- exclusive food suppliers like Spirit
MEDIC = 3 -- medics
SUPPLIER = 4 -- everyone else that sells crap
-- return trader type as int, or nil if error
function get_trader_type(npc)
local st = db.storage[npc:id()]
if not st then return -1 end
local trader = false
if npc:character_community() == "trader" or npc:clsid() == clsid.script_trader or npc:clsid() == clsid.trader then
trader = true
end
if find(npc:section(),"trader") then
trader = true
end
local cini = st.ini
local logic = st.section_logic
if not logic and not trader then return -1 end
local trade_logic = cini and cini:r_string_ex(logic, "trade")
if not trade_logic then return -1 end
if find(trade_logic, "companion") then
return COMPANION
elseif find(trade_logic, "trade_generic_mechanic") then
return MECHANIC
elseif find(trade_logic, "trade_generic_barman") then
return BARMAN
elseif find(trade_logic, "trade_generic_medic") then
return MEDIC
else
return SUPPLIER
end
end
-- return supply level of npc, like suppy_1, supply_2, etc
-- as_number removes the supply_ prefix and only returns as int
function supply_level(npc, as_number)
local profile = trade_manager.get_trade_profile(npc:id(), "cfg_ltx")
-- printf("Profile is %s", profile)
local config = trade_manager.get_trade_cfg(profile)
if not config then return end
local str = config:r_string_ex("trader", "buy_supplies")
if not (str) then
return -- no buy_supplies this is normal
end
local condlist = xr_logic.parse_condlist(npc, "trader", "buy_supplies", str)
str = condlist and xr_logic.pick_section_from_condlist(db.actor, npc, condlist)
if as_number then
local num = str_explode(str, "_")
return tonumber(num[2])
else
return str
end
end
-- collapse several tables into one table, the way sections work in ltx files
-- tables should be in section -> amount format
-- precedence goes up to last table, meaning whatever is in the last table will be the last changes applied
function merge_tables(tables)
local final_table = {}
if #tables > 0 then
copy_table(final_table, tables[1])
if #tables > 1 then
for i=2, #tables do
for k,v in pairs(tables[i]) do
final_table[k] = v
end
end
end
end
return final_table
end
-- used to get the real community of the NPC by checking spawn id
-- author: HarukaSai
function get_real_community(npc, default)
if furniture[npc:name()] then
return "stalker"
end
local community = character_community(npc)
if not blacklisted_comms[community] then
return community
end
local squad_community = get_object_squad(npc):get_squad_community()
if not blacklisted_comms[squad_community] then
return squad_community
else
return default
end
end
-- to_spawn should be table of sections to amount
-- if check_existing is true, only spawns up to that amount in trader inventory. else arbitrarily spawns
function spawn_items(npc, to_spawn, check_existing)
local npc_name = npc:name()
local alive_or_furniture = xr_conditions.is_alive(db.actor, npc) or furniture[npc_name]
if not alive_or_furniture then return end
local supply_table = {}
copy_table(supply_table, to_spawn)
if check_existing then
local function itr_inv(temp, item)
if supply_table[item:section()] and supply_table[item:section()] > 0 then
-- printf("Found 1 of %s", item:section())
supply_table[item:section()] = supply_table[item:section()] - 1
end
end
npc:iterate_inventory(itr_inv)
end
for k,v in pairs(supply_table) do
-- printf("Creating %s of %s", v, k)
for i=1, v do
-- printf("Created %s", k)
alife_create_item(k, npc)
end
end
end
AddScriptCallback("trader_on_restock")
@@ -0,0 +1,213 @@
local gc = game.translate_string
local ratio = utils_xml.screen_ratio()
local use_ingame_time = true
---@type ui_furniture_light.UIFurnitureLight
GUI = nil -- instance, don't touch
class "UIFurnitureLight" (CUIScriptWnd)
function UIFurnitureLight:__init() super()
self:InitControls()
self:InitCallbacks()
end
function UIFurnitureLight:__finalize()
GUI = nil
end
function UIFurnitureLight:InitControls()
self:SetWndRect(Frect():set(0,0,1024,768))
self.wide = (device().width/device().height) > (1024/768 + 0.01)
self:SetAutoDelete(true)
local xml = CScriptXmlInit()
-- xml:ParseFile("ui_sleep_dialog.xml")
xml:ParseFile("ui_furniture_light_dialog.xml")
self.back = xml:InitFrame("background", self)
self.icon = xml:InitStatic("icon", self.back)
self.icon:SetWndSize(vector2():set( self.icon:GetWidth(), self.icon:GetWidth() / ratio ))
self.light_duration = xml:InitTextWnd("light_duration", self.back)
self.btn_pickup = xml:Init3tButton("btn_pickup", self.back)
self:Register(self.btn_pickup, "btn_pickup")
self.btn_close = xml:Init3tButton("btn_close", self.back)
self:Register(self.btn_close, "btn_close")
self.btn_turn_on = xml:Init3tButton("btn_turn_on", self.back)
self:Register(self.btn_turn_on, "btn_turn_on")
self.btn_add_fuel = xml:Init3tButton("btn_add_fuel", self.back)
self:Register(self.btn_add_fuel, "btn_add_fuel")
end
function UIFurnitureLight:ToggleLight()
toggle_light(self.light_id)
self:Close()
end
function UIFurnitureLight:Pickup()
local obj = get_object_by_id(self.light_id)
---@type bind_hf_base.hf_binder_wrapper
local wrapper = obj:binded_object().wrapper
wrapper:pickup()
self:Close()
end
function UIFurnitureLight:Refuel()
local fuel_type = ini_sys:r_string_ex(self.section, "fuel_section") or "charcoal"
if db.actor:object(fuel_type) then
itms_manager.relocate_item_from_actor(db.actor, nil, fuel_type, 1)
local obj = get_object_by_id(self.light_id)
obj:binded_object().wrapper.fuel = 1.0
else
actor_menu.set_msg(1, "I don't have any fuel for this.",3)
end
self:Close()
end
function UIFurnitureLight:InitCallbacks()
self:AddCallback("btn_turn_on", ui_events.BUTTON_CLICKED, self.ToggleLight, self)
self:AddCallback("btn_pickup", ui_events.BUTTON_CLICKED, self.Pickup, self)
self:AddCallback("btn_add_fuel", ui_events.BUTTON_CLICKED, self.Refuel, self)
self:AddCallback("btn_close", ui_events.BUTTON_CLICKED, self.Close, self)
end
function UIFurnitureLight:Initialize()
self.section = get_object_by_id(self.light_id):section()
-- printf("Width: " .. self.icon:GetWidth() .. "| Height: " .. self.icon:GetHeight())
local icon = ini_sys:r_string_ex(self.section, "ui_texture")
-- printf("Light Section: " .. self.section)
if icon then
-- printf("Light Icon: " .. icon)
self.icon:InitTexture(icon)
end
end
function UIFurnitureLight:TestAndShow(obj_id)
self.light_id = obj_id
self:Initialize()
self:ShowDialog(true)
Register_UI("UIFurnitureLight","ui_sleep_dialog")
end
function UIFurnitureLight:Update()
CUIScriptWnd.Update(self)
local wrapper = bind_hf_base.get_wrapper(self.light_id)
local time_secs = wrapper.fuel * wrapper.max_duration
if not use_ingame_time then
time_secs = time_secs / level.get_time_factor()
end
local days = math.floor(time_secs/86400)
local hours = math.floor(math.mod(time_secs, 86400)/3600)
local minutes = math.floor(math.mod(time_secs,3600)/60)
local time_str = string.format("%dd %02dh %02dm",days,hours,minutes)
local fuel_percent = math.ceil(wrapper.fuel * 100)
local clr = utils_xml.get_color_con(wrapper.fuel*100)
local clr_grey = utils_xml.get_color("ui_gray_1")
self.light_duration:SetText(clr .. gc("st_duration") .. ": " .. time_str .. " (" .. fuel_percent .. "%)")
if wrapper.last_state then
self.btn_turn_on:TextControl():SetText(gc("st_turn_off"))
else
self.btn_turn_on:TextControl():SetText(gc("st_turn_on"))
end
end
function UIFurnitureLight:OnTrackButton()
end
function UIFurnitureLight:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if (res == false) then
local bind = dik_to_bind(dik)
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
self:Close()
end
end
end
return res
end
function UIFurnitureLight:Close()
if (self:IsShown()) then
self:HideDialog()
end
--db.actor:give_info_portion("tutorial_sleep")
Unregister_UI("UIFurnitureLight")
end
-------
function toggle_light(obj_id)
local section = alife_object(obj_id):section_name()
local is_on = hf_obj_manager.get_data(obj_id).is_on
if is_on then
hf_obj_manager.update_data(obj_id, {is_on=false})
return
end
local required_tools = parse_list(ini_sys, section, "require_tool")
-- Check for legacy config key (backwards compatibility)
local require_matches = ini_sys:r_bool_ex(section, "require_matches") or false
if require_matches then
table.insert(required_tools, "matches")
table.insert(required_tools, "box_matches")
end
if is_empty(required_tools) then
hf_obj_manager.update_data(obj_id, {is_on=true})
return
end
for _,tool in pairs(required_tools) do
local obj_item = db.actor:object(tool)
if obj_item then
if utils_item.is_degradable(obj_item) then
utils_item.degrade(obj_item, 0.05)
else
utils_item.discharge(obj_item)
end
hf_obj_manager.update_data(obj_id, {is_on=true})
return
end
end
actor_menu.set_msg(1, game.translate_string("st_ui_campfire_prereq"), 3)
end
function start_dialog(obj_id)
if (GUI == nil) then
GUI = UIFurnitureLight()
end
GUI:TestAndShow(obj_id)
--return GUI
end
-------
function on_option_change(mcm)
if mcm then
use_ingame_time = ui_mcm.get("aol_hf/gameplay/use_ingame_time")
end
end
function on_game_start()
RegisterScriptCallback("on_option_change",on_option_change)
on_option_change(ui_mcm and ui_mcm.key_hold)
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,199 @@
-- Workshop Logic
function UseWorkshop(id)
local stash_id = GetStash(id)
CreateTimeEvent("hf_workshop", "get_stash_items_"..id, 0, GiveStashItemsToActor, id, stash_id)
CreateTimeEvent("hf_workshop","open_workshop_"..id,0.1,OpenWorkshop)
end
function CreateStash(id)
local obj = get_object_by_id(id)
-- position under map
local pos = vec_set(obj:position())
pos.y = pos.y - 50
-- create inventory_box_s
return alife_create("workshop_stash",pos,obj:level_vertex_id(),obj:game_vertex_id())
end
function GetStash(id)
local m_data = alife_storage_manager.get_state()
local se_inv_box = m_data.workshop_stashes and m_data.workshop_stashes[id] and alife_object(m_data.workshop_stashes[id])
local stash_id = nil
if not se_inv_box then
se_inv_box = CreateStash(id)
end
-- shouldn't be possible but very safe incase some sort of save corruption
if not (IsInvbox(nil,se_inv_box:clsid())) then
if m_data.workshop_stashes then
m_data.workshop_stashes[id] = nil
end
return
end
-- force strictly online (not sure this is needed)
alife():set_switch_online(se_inv_box.id,true)
alife():set_switch_offline(se_inv_box.id,false)
-- Save container
if not (m_data.workshop_stashes) then
m_data.workshop_stashes = {}
end
m_data.workshop_stashes[id] = se_inv_box.id
stash_id = se_inv_box.id
-- Object will come online next update so wait
-- CreateTimeEvent(id, "move_stash", 0, GiveStashItemsToActor, id, stash_id)
return stash_id
end
local function UseStash(id)
local box = id and level.object_by_id(id)
-- repeat timed event
if not box then return false end
hide_hud_inventory()
box:use(db.actor)
return true -- destroy timed event
end
function GiveStashItemsToActor(workshop_id, stash_id)
local items = {}
if stash_id then
local stash_obj = get_object_by_id(stash_id)
if not stash_obj then return end
stash_obj:iterate_inventory_box( function(temp, obj)
table.insert(items, obj:id())
stash_obj:transfer_item(obj, db.actor)
end, stash_obj)
end
zzz_workshop_return_items.item_ids = items
zzz_workshop_return_items.stash_id = stash_id
return true
end
function OpenWorkshop()
workshop_ui_ref = ui_workshop.get_workshop_ui(nil, nil, {false,false,false,false,false}, false)
if (workshop_ui_ref) then
workshop_ui_ref:ShowDialog(true)
end
return true
end
-- UI
local ratio = utils_xml.screen_ratio()
GUI = nil -- instance, don't touch
class "UIFurnitureWorkshop" (CUIScriptWnd)
function UIFurnitureWorkshop:__init() super()
self:InitControls()
self:InitCallbacks()
end
function UIFurnitureWorkshop:__finalize()
GUI = nil
end
function UIFurnitureWorkshop:InitControls()
self:SetWndRect(Frect():set(0,0,1024,768))
self.wide = (device().width/device().height) > (1024/768 + 0.01)
self:SetAutoDelete(true)
local xml = CScriptXmlInit()
-- xml:ParseFile("ui_sleep_dialog.xml")
xml:ParseFile("ui_furniture_workshop_dialog.xml")
self.back = xml:InitFrame("background", self)
self.icon = xml:InitStatic("icon", self.back)
self.icon:SetWndSize(vector2():set( self.icon:GetWidth(), self.icon:GetWidth() / ratio ))
self.btn_pickup = xml:Init3tButton("btn_pickup", self.back)
self:Register(self.btn_pickup, "btn_pickup")
self.btn_close = xml:Init3tButton("btn_close", self.back)
self:Register(self.btn_close, "btn_close")
self.btn_access_stash = xml:Init3tButton("btn_access_stash", self.back)
self:Register(self.btn_access_stash, "btn_access_stash")
self.btn_use_workshop = xml:Init3tButton("btn_use_workshop", self.back)
self:Register(self.btn_use_workshop, "btn_use_workshop")
end
function UIFurnitureWorkshop:UseWorkshop()
UseWorkshop(self.obj_id)
self:Close()
end
function UIFurnitureWorkshop:AccessStash()
local stash_id = GetStash(self.obj_id)
CreateTimeEvent("hf_workshop","open_stash_"..stash_id,0,UseStash,stash_id)
self:Close()
end
function UIFurnitureWorkshop:Pickup()
local obj = get_object_by_id(self.obj_id)
---@type bind_hf_base.hf_binder_wrapper
local wrapper = obj:binded_object().wrapper
wrapper:pickup()
self:Close()
end
function UIFurnitureWorkshop:InitCallbacks()
self:AddCallback("btn_access_stash", ui_events.BUTTON_CLICKED, self.AccessStash, self)
self:AddCallback("btn_pickup", ui_events.BUTTON_CLICKED, self.Pickup, self)
self:AddCallback("btn_use_workshop", ui_events.BUTTON_CLICKED, self.UseWorkshop, self)
self:AddCallback("btn_close", ui_events.BUTTON_CLICKED, self.Close, self)
end
function UIFurnitureWorkshop:TestAndShow(obj_id)
self.obj_id = obj_id
self:ShowDialog(true)
Register_UI("UIFurnitureWorkshop","ui_hf_workshop")
end
function UIFurnitureWorkshop:Update()
CUIScriptWnd.Update(self)
end
function UIFurnitureWorkshop:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if (res == false) then
local bind = dik_to_bind(dik)
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
self:Close()
end
end
end
return res
end
function UIFurnitureWorkshop:Close()
if (self:IsShown()) then
self:HideDialog()
end
Unregister_UI("UIFurnitureWorkshop")
end
-------
function start_dialog(obj_id)
if (GUI == nil) then
GUI = UIFurnitureWorkshop()
end
GUI:TestAndShow(obj_id)
--return GUI
end
@@ -0,0 +1,271 @@
weapon_displays = {}
weapon_display_slots = {}
function on_game_start()
RegisterScriptCallback("ActorMenu_on_item_after_move", ActorMenu_on_item_after_move)
RegisterScriptCallback("ActorMenu_on_item_before_move", ActorMenu_on_item_before_move)
RegisterScriptCallback("actor_on_item_before_pickup", actor_on_item_before_pickup)
RegisterScriptCallback("game_object_on_net_spawn", game_object_on_net_spawn)
RegisterScriptCallback("save_state", save_state)
RegisterScriptCallback("load_state", load_state)
-- Patch the displayed items into world items, so that npc's don't pick them up
local base_is_world_item = game_setup.is_world_item
function game_setup.is_world_item(id)
for case_id, case_guns in pairs(weapon_displays) do
for item_id, world_id in pairs(case_guns) do
if id == world_id then
return true
end
end
end
return base_is_world_item(id)
end
end
valid_actor_bags = {
[EDDListType.iActorBag] = true,
[EDDListType.iActorSlot] = true
}
valid_stash = {}
valid_display_kind = {}
-- probably a bad idea to mix display metadata with pos/rot offsets in seperate sections, but in the same LTX
local ini_displays = ini_file("items\\settings\\hideout_furniture\\displays\\displays.ltx")
ini_displays:section_for_each(function(section)
local slots = ini_displays:r_float_ex(section, "slots")
if not slots then return end -- exit early if there is no number of slots
valid_stash[section] = slots
local valid_classes = ini_displays:r_list(section, "valid_classes")
valid_display_kind[section] = {}
for i, class in pairs(valid_classes) do
valid_display_kind[section][class] = true
end
end)
---@param se_obj game_object
function game_object_on_net_spawn(se_obj)
for case_id, case_guns in pairs(weapon_displays) do
for item_id, world_id in pairs(case_guns) do
if se_obj:id() == world_id then
CreateTimeEvent("collector","freeze_" .. se_obj:id(), 0, freeze_item, se_obj:id(), item_id)
return
end
end
end
end
function ActorMenu_on_item_before_move(flags, case_id, item, mode, bag_from)
if not (case_id and item and mode == "loot") then return end
local case_obj = get_object_by_id(case_id)
local case_sec = case_obj:section()
if case_obj and case_sec and valid_stash[case_sec] and valid_actor_bags[bag_from] then
local sec = item:section()
local kind = SYS_GetParam(0, sec, "kind")
local case_slot_count = valid_stash[case_sec]
if weapon_displays[case_id] and size_table(weapon_displays[case_id]) >= case_slot_count or not(kind and valid_display_kind[case_sec][kind]) then
flags.ret_value = false
end
end
end
function ActorMenu_on_item_after_move(case_id, item, mode, bag_from)
if not (case_id and item and mode == "loot") then return end
local case_obj = get_object_by_id(case_id)
local case_sec = case_obj:section()
if case_obj and case_sec and valid_stash[case_sec] then
local sec = item:section()
local kind = SYS_GetParam(0, sec, "kind")
local item_id = item:id()
if valid_actor_bags[bag_from] and kind and valid_display_kind[case_sec][kind] then
if weapon_displays[case_id] and weapon_displays[case_id][item_id] then
RemoveTimeEvent("collector","remove_" .. weapon_displays[case_id][item_id])
else
if not weapon_displays[case_id] then
weapon_displays[case_id] = {}
weapon_display_slots[case_id] = {}
end
local ini_wd = ini_file("items\\settings\\hideout_furniture\\displays\\displays.ltx")
local se_case = alife_object(case_id)
local pos = vector():set(case_obj:position())
local case_slot_count = valid_stash[case_sec]
local gun_number = get_first_open_slot(weapon_display_slots[case_id], case_slot_count)
local gun_section = ini_sys:r_string_ex(sec,"parent_section") or sec
-- Rotation --
-- Pull slot angle for stash
local angle = vector():set(se_case.angle)
local angle_q = aol_rotation.Quaternion(angle)
local angle_str = ini_wd:r_string_ex(case_sec .. "_angles",gun_number)
-- Pull angle for gun slot
local case_slot_angle = vector():set(string_to_vector(angle_str)) -- get specific angle for gun slot
case_slot_angle = case_slot_angle:mul(0.017453292519943295769236907684886) -- convert to radian
-- Pull angle for gun
local gun_adjust_str = ini_wd:r_string_ex("gun_angle_offsets",gun_section) or "0,0,0"
local gun_adjust_angle = string_to_vector(gun_adjust_str)
gun_adjust_angle = gun_adjust_angle:mul(0.017453292519943295769236907684886)
local angle_offset = case_slot_angle:add(gun_adjust_angle)
local angle_offset_q = aol_rotation.Quaternion(angle_offset)
local q_rotation = angle_offset_q:multiply(angle_q)
-- Location --
local pos_offset = vector():set(0,0,0)
-- Add position offset (weapon-specific, in vanilla)
local position_offset_str = ini_sys:r_string_ex(sec, "position")
if position_offset_str then
local vec = string_to_vector(position_offset_str)
vec.x = 0 -- ignore x axis offset
pos_offset:add(vec)
end
-- Pull position adjustment for gun (weapon-display-specific)
local ini_gun_offsets = ini_file("items\\settings\\hideout_furniture\\item_offsets\\items.ltx")
local pos_gun_adjust = ini_gun_offsets:r_string_ex("gun_position_offsets",gun_section)
if pos_gun_adjust then
-- Rotate offset to align with orientation of stash
pos_offset:add(string_to_vector(pos_gun_adjust))
end
-- Rotate position offset by unique gun rotation offset
pos_offset = angle_offset_q:rotate_vector(pos_offset)
-- Pull slot positions for stash
local slot_pos_str = ini_wd:r_string_ex(case_sec .. "_positions",gun_number)
local slot_pos = vector():set(string_to_vector(slot_pos_str))
-- pos_offset = aol_rotation.rotate_vector_by_euler(pos_offset, se_case.angle)
pos_offset:add(slot_pos)
pos_offset = angle_q:rotate_vector(pos_offset)
pos = pos:add(pos_offset)
-- Create object at pos
local world_se_obj = alife_create_item(sec, { pos, db.actor:level_vertex_id(), db.actor:game_vertex_id() })
-- Rotate object
world_se_obj.angle = q_rotation:to_euler_angles()
-- world_se_obj.angle = angle
-- Set invisible to AI (prevent pickup)
local data = utils_stpk.get_weapon_data(world_se_obj)
local remove_flags = 16 + 128
local flag_mask = bit_not(remove_flags)
data.object_flags = bit_and(data.object_flags, flag_mask)
utils_stpk.set_weapon_data(data, world_se_obj)
release_item_manager.unmark_item(world_se_obj.id)
weapon_displays[case_id][item_id] = world_se_obj.id
weapon_display_slots[case_id][gun_number] = world_se_obj.id
end
elseif weapon_displays[case_id] and weapon_displays[case_id][item_id] then
CreateTimeEvent("collector","remove_" .. weapon_displays[case_id][item_id], 0, remove_displayed_item, item_id, weapon_displays[case_id][item_id], case_id)
end
end
end
function string_to_vector(string)
local t = str_explode(string,",")
for k, str in pairs(t) do
t[k] = string.gsub(str, "%s+", "") -- remove whitespaces
t[k] = tonumber(t[k])
--printf(str)
end
return vector():set(t[1], t[2], t[3])
end
function get_first_open_slot(t, max_slots)
for i=1, max_slots do
if not t[i] then
return i
end
end
end
function actor_on_item_before_pickup(obj, flags)
for case_id, case_guns in pairs(weapon_displays) do
for item_id, world_id in pairs(case_guns) do
if obj:id() == world_id then
--printf("NO PICKUP")
flags.ret_value = false
end
end
end
end
function freeze_item(id, item_id)
local obj = level.object_by_id(id)
local phys = obj and obj:get_physics_shell()
if phys then
-- Show silencer/launcher if stashed item has the attachment
if utils_item.has_attached_silencer(get_object_by_id(item_id)) then
obj:set_bone_visible("wpn_silencer", true, true)
end
if utils_item.has_attached_gl(get_object_by_id(item_id)) then
obj:set_bone_visible("wpn_launcher", true, true)
end
phys:freeze()
return true
end
return false
end
function remove_displayed_item(item_id, world_item_id, case_id)
local obj = alife_object(world_item_id)
if obj then
alife_release(obj) -- USE safe_release_manager.release(se_obj) ??????
weapon_displays[case_id][item_id] = nil
for slot, id in pairs(weapon_display_slots[case_id]) do
if id == world_item_id then
weapon_display_slots[case_id][slot] = nil
break
end
end
if size_table(weapon_displays[case_id]) == 0 then
weapon_displays[case_id] = nil
weapon_display_slots[case_id] = nil
end
RemoveTimeEvent("collector","freeze_" .. world_item_id)
--printf("DISPLAYED ITEM REMOVED")
return true
end
--printf("TRYING TO REMOVE")
return false
end
function vector_rotate_y_radian(v, angle)
--angle = angle * 0.017453292519943295769236907684886
local c = math.cos (angle)
local s = math.sin (angle)
return vector():set(v.x * c - v.z * s, v.y, v.x * s + v.z * c)
end
function angle_to_radian(angle)
return angle * 0.017453292519943295769236907684886
end
function save_state(m_data)
m_data.weapon_displays = weapon_displays
m_data.weapon_display_slots = weapon_display_slots
end
function load_state(m_data)
weapon_displays = m_data.weapon_displays or {}
weapon_display_slots = m_data.weapon_display_slots or {}
end
@@ -0,0 +1,401 @@
local cr = {}
cr.added = {}
cr.index_map = {}
cr.overrides = {}
cr.new_sections = {}
cr.deletions = {}
local known_recipe = {}
function print_dbg(text, ...)
if true then
-- printf( "workshop auto: | %s | "..text ,time_global(), ...)
end
end
local function validate_recipe(craft_string)
local t = str_explode(craft_string,",")
if (#t == 6 or #t == 8 or #t == 10) then
local tool = tonumber(t[1])
if tool < 0 or tool > 5 then
print_dbg("Invalid tool %s! Must be 0-5", t[1])
return false
end
if not string.find(t[2], "recipe") then
print_dbg("Invalid recipe %s!", t[2])
return false
end
for i=3,#t,2 do
local item = t[i]
local amt = tonumber(t[i+1])
if not ini_sys:section_exist(item) then
print_dbg("Invalid component %s!", item)
return false
end
if not amt or amt <= 0 then
print_dbg("Invalid amount for component %s!", amt)
return false
end
end
return true
else
print_dbg("Invalid length of result table!")
return false
end
end
-- Add a new recipe to workshops. You can add as many recipes as you want for an item, the only caveat is you cannot add a recipe with an 'override' (more below).
-- Arguments:
-- index - Index page of the workshop item (analogous to what page it's on, e.g. 1-6 by default. More can be added with add_index)
-- sec - Section of the item to be crafted
-- recipe_string - Valid recipe string for the item. Needs to follow the format in craft.ltx e.g. "1, recipe_basic_0, broken_detector,1,prt_i_resistors,8,prt_i_transistors,7,prt_i_capacitors,8"
-- returns true on success, false on failure with printed reason
function add_new_recipe(index, sec, recipe_string)
if index > 6 and not cr.new_sections[index] then return false end
if not ini_sys:section_exist(sec) then return false end
if not validate_recipe(recipe_string) then return false end
if cr.overrides[sec] then
print_dbg("Recipe %s already has an override recipe, returning", sec)
return false
end
if not cr.added[sec] then
cr.added[sec] = {}
cr.index_map[sec] = index
end
local length = #cr.added[sec]
cr.added[sec][length + 1] = recipe_string
end
-- Delete any existing recipes not added by autoinject (e.g. that were predefined in craft.ltx)
function clear_existing_recipes(sec)
if not ini_sys:section_exist(sec) then return false end
if not cr.deletions[sec] then cr.deletions[sec] = true end
return true
end
-- Specify a crafting recipe that will supersede all other crafting recipes for that item. Arguments similar to above.
-- returns true on success, false on failure with printed reason
function add_override_recipe(sec, recipe_string, index)
if not ini_sys:section_exist(sec) then return false end
if not validate_recipe(recipe_string) then return false end
if cr.added[sec] then
print_dbg("Recipe %s already has additive recipes, returning", sec)
return false
end
if not index then index = 1 end
cr.index_map[sec] = index
cr.overrides[sec] = recipe_string
return true
end
function remove_existing_recipes(sec)
if cr.overrides[sec] then
print_dbg("Recipe %s already has an override recipe, returning")
return false
end
cr.overrides[sec] = "none"
return true
end
-- Add a new page in the crafting menu. ID must be greater than 6. First-come, first-serve basis.
function add_section(id, name)
if id < 7 then
print_dbg("Index already exists")
return
end
if not cr.new_sections[id] then
cr.new_sections[id] = name
return true
else
print_dbg("Section already exists: %s", cr.new_sections[id])
return false
end
end
-- to bypass encyclopedia
function ui_workshop.UIWorkshopCraft:ListRecipes()
-- Recipes showcase
if is_empty(self.CC["recipe"].cell) then
local inv, size_t = {}, 0
for recipe,_ in pairs(GetItemList("recipe")) do
size_t = size_t + 1
inv[size_t] = recipe
end
self.CC["recipe"]:Reinit(inv)
end
-- Show/hide unlocked/locked recipes items
for recipe,_ in pairs(GetItemList("recipe")) do
local state = false
print_dbg("Checking recipe %s", recipe)
if ui_pda_encyclopedia_tab.is_unlocked_note("encyclopedia__notes_" .. recipe) or known_recipe[recipe] then
state = true
end
self.recipes_items[recipe] = state
end
for idx,ci in pairs(self.CC["recipe"].cell) do
if ci:IsShown() then
if self.recipes_items[ci.section] then
ci:Colorize("def")
else
ci:Colorize("hide")
end
end
end
end
function ui_workshop.UIWorkshopCraft:LoadRecipes()
-- Less parts on achievement
local ach = 0
if (game_achievements.has_achievement("artificer_eagerness")) then
ach = 1
end
local ind = 1
local ini = itms_manager.ini_craft
while ini:section_exist(tostring(ind)) do
local ind_str = tostring(ind)
local n = ini:line_count(ind_str) or 0
self.recipes[ind] = {}
for i=0, n-1 do
local result, id, value = ini:r_line(ind_str , i , "", "")
if (id == "title") then
self.recipes_type[ind] = value
end
id = string.sub(id,3)
-- hijack and replace
if cr.overrides[id] then
print_dbg("UIWorkshop - Hijacking recipe for %s", id)
local craft_string = cr.overrides[id]
if craft_string then
-- if craft_string ~= "override" then
-- print_dbg("Craft recipe for %s is %s", id, craft_string)
-- local t = str_explode(craft_string,",")
-- add_recipe(self.recipes, ind, t, ach, id)
-- end
else
local t = str_explode(value,",")
add_recipe(self.recipes, ind, t, ach, id)
end
elseif cr.deletions[id] then
-- do nothing lel
elseif ini_sys:section_exist(id) then
local t = str_explode(value,",")
add_recipe(self.recipes, ind, t, ach, id)
elseif (id ~= "tle") then
printe("! UIWorkshopCraft:LoadRecipes() | section [%s] not found!",id)
end
end
ind = ind + 1
end
for k,v in pairs(cr.new_sections) do
self.recipes_type[k] = v
self.recipes[k] = {}
end
for section,craft_string in pairs(cr.overrides) do
if craft_string ~= "override" then
local index = cr.index_map[section]
local t = str_explode(craft_string,",")
add_recipe(self.recipes, index, t, ach, section)
end
end
for section,recipes in pairs(cr.added) do
local index = cr.index_map[section]
for x,craft_string in pairs(recipes) do
local t = str_explode(craft_string, ",")
add_recipe(self.recipes, index, t, ach, section)
end
end
for i=1,#self.recipes_type do
local _itm = ui_workshop.list_element(i, self.recipes_type[i])
self.list_menu:AddExistingItem(_itm)
end
end
function item_recipe.UIRecipe:LoadRecipes()
local ini_craft = itms_manager.ini_craft
-- Less parts on achievement
local ach = 0
if (game_achievements.has_achievement("artificer_eagerness")) then
ach = 1
end
local ind = 1
while ini_craft:section_exist(tostring(ind)) do
local ind_str = tostring(ind)
local n = ini_craft:line_count(ind_str) or 0
for i=0, n-1 do
local result, id, value = ini_craft:r_line(ind_str , i , "", "")
id = string.sub(id,3)
if cr.overrides[id] then
print_dbg("UIWorkshop - Hijacking recipe for %s", id)
local craft_string = cr.overrides[id]
if craft_string then
if craft_string ~= "override" then
print_dbg("Craft recipe for %s is %s", id, craft_string)
local t = str_explode(craft_string,",")
add_recipe_ui(self.recipes, self.section ,self.toolkit, t, ach, id)
end
else
local t = str_explode(value,",")
add_recipe_ui(self.recipes, self.section ,self.toolkit, t, ach, id)
end
elseif ini_sys:section_exist(id) then
local t = str_explode(value,",")
add_recipe_ui(self.recipes, self.section ,self.toolkit, t, ach, id)
elseif (id ~= "tle") then
printe("! workshop_craft_ui:LoadRecipes() | section [%s] not found!",id)
end
end
ind = ind + 1
end
for section,recipes in pairs(cr.added) do
local index = cr.index_map[section]
for x,craft_string in pairs(recipes) do
local t = str_explode(craft_string, ",")
print_dbg("(Section %s) Adding for %s custom recipe %s", self.section, section, craft_string)
add_recipe_ui(self.recipes, self.section ,self.toolkit, t, ach, section)
end
end
end
function item_recipe.func_recipe(obj)
local sec = obj:section()
print_dbg("intercept recipe %s", sec)
if (not IsItem("recipe",sec)) then
return
end
if (not ui_pda_encyclopedia_tab.is_unlocked_note("encyclopedia__notes_" .. sec)) then
SendScriptCallback("actor_on_interaction", "notes", nil, sec)
--alife_release(obj)
else
actor_menu.set_msg(1, game.translate_string("st_recipe_is_known"),3)
end
-- add to backup cache as well
if not known_recipe[sec] then
print_dbg("Adding %s to backup cache", sec)
known_recipe[sec] = true
end
-- effect
local hud = get_hud()
if (hud) then
hide_hud_inventory()
end
item_recipe.start(sec)
end
--recipes, workshop section, table of requirements, achievement, item section
function add_recipe(recipes, ind, t, ach, id)
if (#t == 6) or (#t == 8) or (#t == 10) then
if not recipes[ind] then recipes[ind] = {} end
local x = #recipes[ind] + 1
recipes[ind][x] = {}
recipes[ind][x].sec = id
recipes[ind][x].tool = tonumber(t[1]) or 1
recipes[ind][x].rsp = t[2]
if t[3] and t[4] then -- support item 1
if ini_sys:section_exist(tostring(t[3])) then
local amt = tonumber(t[4])
recipes[ind][x][1] = {tostring(t[3]), (amt > 4) and (amt - ach) or amt}
else
printe("! Workshop UI craft | componenet section [%s] not found for [%s] recipe!", tostring(t[3]), id)
end
end
if t[5] and t[6] then -- support item 2
if ini_sys:section_exist(tostring(t[5])) then
local amt = tonumber(t[6])
recipes[ind][x][2] = {tostring(t[5]), (amt > 4) and (amt - ach) or amt}
else
printe("! UIWorkshopCraft:LoadRecipes() | componenet section [%s] not found for [%s] recipe!", tostring(t[5]), id)
end
end
if t[7] and t[8] then -- support item 3
if ini_sys:section_exist(tostring(t[7])) then
local amt = tonumber(t[8])
recipes[ind][x][3] = {tostring(t[7]), (amt > 4) and (amt - ach) or amt}
else
printe("! UIWorkshopCraft:LoadRecipes() | componenet section [%s] not found for [%s] recipe!", tostring(t[7]), id)
end
end
if t[9] and t[10] then -- support item 4
if ini_sys:section_exist(tostring(t[9])) then
local amt = tonumber(t[10])
recipes[ind][x][4] = {tostring(t[9]), (amt > 4) and (amt - ach) or amt}
else
printe("! UIWorkshopCraft:LoadRecipes() | componenet section [%s] not found for [%s] recipe!", tostring(t[9]), id)
end
end
end
end
function add_recipe_ui(recipes, section, toolkit, t, ach, id)
if ((#t == 6) or (#t == 8) or (#t == 10) )and (t[2] == section) then
local x = #recipes + 1
recipes[x] = {}
recipes[x].sec = id
toolkit = ui_workshop.workshop_toolkits[tonumber(t[1]) or 1]
if t[3] and t[4] then -- support item 1
if ini_sys:section_exist(tostring(t[3])) then
local amt = tonumber(t[4])
recipes[x][1] = {tostring(t[3]), (amt > 4) and (amt - ach) or amt}
else
printe("! Workshop UI craft | componenet section [%s] not found for [%s] recipe!", tostring(t[3]), id)
end
end
if t[5] and t[6] then -- support item 2
if ini_sys:section_exist(tostring(t[5])) then
local amt = tonumber(t[6])
recipes[x][2] = {tostring(t[5]), (amt > 4) and (amt - ach) or amt}
else
printe("! UIWorkshopCraft:LoadRecipes() | componenet section [%s] not found for [%s] recipe!", tostring(t[5]), id)
end
end
if t[7] and t[8] then -- support item 3
if ini_sys:section_exist(tostring(t[7])) then
local amt = tonumber(t[8])
recipes[x][3] = {tostring(t[7]), (amt > 4) and (amt - ach) or amt}
else
printe("! UIWorkshopCraft:LoadRecipes() | componenet section [%s] not found for [%s] recipe!", tostring(t[7]), id)
end
end
if t[9] and t[10] then -- support item 4
if ini_sys:section_exist(tostring(t[9])) then
local amt = tonumber(t[10])
recipes[x][4] = {tostring(t[9]), (amt > 4) and (amt - ach) or amt}
else
printe("! UIWorkshopCraft:LoadRecipes() | componenet section [%s] not found for [%s] recipe!", tostring(t[9]), id)
end
end
end
end
local function save_state(mdata)
mdata.known_recipe = known_recipe
end
function load_state(mdata)
known_recipe = mdata.known_recipe or {}
end
function on_game_start()
RegisterScriptCallback("save_state",save_state)
RegisterScriptCallback("load_state",load_state)
end
@@ -0,0 +1,23 @@
item_ids = {}
stash_id = nil
function return_items()
if stash_id == nil then return end
local stash_obj = get_object_by_id(stash_id)
if stash_obj == nil then return end
for i, id in ipairs(item_ids) do
local item_obj = get_object_by_id(id)
if item_obj then
db.actor:transfer_item(item_obj, stash_obj)
end
end
item_ids = {}
stash_id = nil
end
ui_workshop_UIWorkshop_Close = ui_workshop.UIWorkshop.Close
ui_workshop.UIWorkshop.Close = function(self)
ui_workshop_UIWorkshop_Close(self)
return_items()
end