Initial Commit

This commit is contained in:
2026-08-12 13:36:14 -04:00
parent cdedb1cb5e
commit 19e4ac0cce
22 changed files with 30278 additions and 1 deletions
@@ -0,0 +1,131 @@
PaintMyKI5 = PaintMyKI5 or {}
PaintMyKI5.PaintInventory = PaintMyKI5.PaintInventory or {}
local PaintInventory = PaintMyKI5.PaintInventory
local EPSILON = 0.0001
local EMPTY_BUCKET = "Base.PaintbucketEmpty"
local function getItems(character, itemType)
if not character or not character:getInventory() then return nil end
return character:getInventory():getAllTypeRecurse(itemType)
end
local function getUses(item)
local useDelta = item:getUseDelta()
if not useDelta or useDelta <= 0 then return 0 end
return item:getCurrentUsesFloat() / useDelta
end
function PaintInventory.countUses(character, itemType)
local items = getItems(character, itemType)
if not items then return 0 end
local total = 0
for index = 0, items:size() - 1 do
total = total + getUses(items:get(index))
end
return total
end
function PaintInventory.checkRequirements(character, requirements)
local missing = {}
for _, requirement in ipairs(requirements or {}) do
local available = PaintInventory.countUses(character, requirement.item)
if available + EPSILON < requirement.uses then
table.insert(missing, {
item = requirement.item,
required = requirement.uses,
available = available,
})
end
end
return #missing == 0, missing
end
local function buildPlan(character, requirements)
local enough = PaintInventory.checkRequirements(character, requirements)
if not enough then return nil end
local plan = {}
for _, requirement in ipairs(requirements) do
local remaining = requirement.uses
local items = getItems(character, requirement.item)
for index = 0, items:size() - 1 do
if remaining <= EPSILON then break end
local item = items:get(index)
local oldDelta = item:getCurrentUsesFloat()
local consumed = math.min(remaining, getUses(item))
local newDelta = oldDelta - consumed * item:getUseDelta()
local mutation = {
item = item,
container = item:getContainer(),
oldDelta = oldDelta,
newDelta = newDelta,
}
if not mutation.container then return nil end
if newDelta <= EPSILON then
mutation.replacement = instanceItem(EMPTY_BUCKET)
if not mutation.replacement then return nil end
end
table.insert(plan, mutation)
remaining = remaining - consumed
end
if remaining > EPSILON then return nil end
end
return plan
end
local function applyMutation(character, mutation)
if not mutation.replacement then
mutation.item:setUsedDelta(mutation.newDelta)
mutation.deltaApplied = true
sendItemStats(mutation.item)
return
end
local added = mutation.container:AddItem(mutation.replacement)
if not added then error("could not add empty paint bucket") end
mutation.replacementAdded = true
sendAddItemToContainer(mutation.container, mutation.replacement)
character:removeFromHands(mutation.item)
mutation.container:DoRemoveItem(mutation.item)
mutation.originalRemoved = true
sendRemoveItemFromContainer(mutation.container, mutation.item)
end
local function rollBackMutation(mutation)
if mutation.replacement then
if mutation.originalRemoved then
mutation.item:setUsedDelta(mutation.oldDelta)
local restored = mutation.container:AddItem(mutation.item)
if not restored then return false end
sendAddItemToContainer(mutation.container, mutation.item)
mutation.originalRemoved = false
end
if mutation.replacementAdded then
mutation.container:DoRemoveItem(mutation.replacement)
sendRemoveItemFromContainer(mutation.container, mutation.replacement)
mutation.replacementAdded = false
end
elseif mutation.deltaApplied then
mutation.item:setUsedDelta(mutation.oldDelta)
sendItemStats(mutation.item)
mutation.deltaApplied = false
end
return true
end
function PaintInventory.consumeRequirements(character, requirements)
local plan = buildPlan(character, requirements)
if not plan then return false end
local applied = {}
for _, mutation in ipairs(plan) do
local ok = pcall(applyMutation, character, mutation)
if not ok then
pcall(rollBackMutation, mutation)
for index = #applied, 1, -1 do
pcall(rollBackMutation, applied[index])
end
return false
end
table.insert(applied, mutation)
end
return true
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,92 @@
require "PaintMyKI5/PaintMyKI5VehiclePaintData"
PaintMyKI5 = PaintMyKI5 or {}
PaintMyKI5.PaintRequirements = PaintMyKI5.PaintRequirements or {}
local PaintRequirements = PaintMyKI5.PaintRequirements
local INTERACTION_AREAS = {
"Engine", "TruckBed", "SeatFrontLeft", "SeatLeft", "SeatFrontRight", "SeatRight"
}
local function getVehicleId(vehicle)
if not vehicle or not vehicle.getScript then return nil end
local script = vehicle:getScript()
return script and script:getFullName() or nil
end
local function isCarId(vehicleId)
local shortId = vehicleId and string.match(vehicleId, "[^.]+$") or nil
return shortId and not string.match(string.lower(shortId), "^trailer")
end
function PaintRequirements.getVehicleData(vehicle)
local data = PaintMyKI5.VehiclePaintData
local vehicleId = getVehicleId(vehicle)
if not data or not data.vehicles or not isCarId(vehicleId) then return nil end
return data.vehicles[vehicleId]
end
function PaintRequirements.getTextureData(vehicle, skinIndex)
local vehicleData = PaintRequirements.getVehicleData(vehicle)
if not vehicleData or type(skinIndex) ~= "number" or skinIndex % 1 ~= 0 then return nil end
for _, textureData in ipairs(vehicleData.textures or {}) do
if textureData.skinIndex == skinIndex then return textureData end
end
return nil
end
function PaintRequirements.getBucketUses()
local data = PaintMyKI5.VehiclePaintData
local uses = data and tonumber(data.bucketUses) or nil
if not uses or uses <= 0 then return nil end
return uses
end
function PaintRequirements.getRequirements(vehicle, skinIndex)
local textureData = PaintRequirements.getTextureData(vehicle, skinIndex)
local bucketUses = PaintRequirements.getBucketUses()
if not textureData or not bucketUses then return nil end
local requirements = {}
local total = 0
for _, paint in ipairs(textureData.paints or {}) do
local uses = tonumber(paint.uses)
if type(paint.item) ~= "string" or not uses or uses <= 0 then return nil end
table.insert(requirements, {
item = paint.item,
percent = tonumber(paint.percent) or 0,
uses = uses,
})
total = total + uses
end
if math.abs(total - bucketUses) > 0.001 then return nil end
return requirements, textureData
end
function PaintRequirements.isSupported(vehicle)
return PaintRequirements.getVehicleData(vehicle) ~= nil
end
function PaintRequirements.findInteractionArea(vehicle, character)
if not vehicle or not character then return nil end
local part = vehicle:getUseablePart(character)
if part and part:getArea() then return part:getArea() end
local script = vehicle:getScript()
if not script then return nil end
for _, area in ipairs(INTERACTION_AREAS) do
if script:getAreaById(area) then return area end
end
return nil
end
function PaintRequirements.isValidTarget(character, vehicle, skinIndex, area)
if not character or not vehicle or vehicle:isRemovedFromWorld() then return false end
if character:getVehicle() then return false end
if math.abs(vehicle:getCurrentSpeedKmHour()) > 0.8 then return false end
if type(skinIndex) ~= "number" or skinIndex % 1 ~= 0 then return false end
if skinIndex < 0 or skinIndex >= vehicle:getSkinCount() then return false end
if vehicle:getSkinIndex() == skinIndex then return false end
if not PaintRequirements.getTextureData(vehicle, skinIndex) then return false end
if not area or not vehicle:isInArea(area, character) then return false end
return true
end
@@ -0,0 +1,115 @@
require "TimedActions/ISBaseTimedAction"
require "PaintMyKI5/PaintRequirements"
require "PaintMyKI5/PaintInventory"
ISPaintMyKI5Vehicle = ISBaseTimedAction:derive("ISPaintMyKI5Vehicle")
function ISPaintMyKI5Vehicle:isValid()
local area = PaintMyKI5.PaintRequirements.findInteractionArea(
self.vehicle, self.character
)
if not PaintMyKI5.PaintRequirements.isValidTarget(
self.character, self.vehicle, self.skinIndex, area
) then
return false
end
local requirements = PaintMyKI5.PaintRequirements.getRequirements(
self.vehicle, self.skinIndex
)
if not requirements then return false end
return PaintMyKI5.PaintInventory.checkRequirements(self.character, requirements)
end
function ISPaintMyKI5Vehicle:waitToStart()
self.character:faceThisObject(self.vehicle)
return self.character:shouldBeTurning()
end
function ISPaintMyKI5Vehicle:update()
self.character:faceThisObject(self.vehicle)
self.character:setMetabolicTarget(Metabolics.MediumWork)
end
function ISPaintMyKI5Vehicle:start()
self.originalSkinIndex = self.vehicle:getSkinIndex()
self:setActionAnim(CharacterActionAnims.Paint)
self:setOverrideHandModels("PaintBrush", nil)
self.sound = self.character:playSound("Painting")
end
function ISPaintMyKI5Vehicle:serverStart()
self.originalSkinIndex = self.vehicle and self.vehicle:getSkinIndex() or nil
end
function ISPaintMyKI5Vehicle:stop()
if self.sound then self.character:stopOrTriggerSound(self.sound) end
ISBaseTimedAction.stop(self)
end
function ISPaintMyKI5Vehicle:perform()
if self.sound then self.character:stopOrTriggerSound(self.sound) end
ISBaseTimedAction.perform(self)
end
local function applySkin(vehicle, skinIndex)
vehicle:setSkinIndex(skinIndex)
if isServer() then
vehicle:transmitSkinIndex()
else
vehicle:updateSkin()
end
end
function ISPaintMyKI5Vehicle:complete()
if isClient() then return true end
local area = PaintMyKI5.PaintRequirements.findInteractionArea(
self.vehicle, self.character
)
if not PaintMyKI5.PaintRequirements.isValidTarget(
self.character, self.vehicle, self.skinIndex, area
) then
return false
end
if self.originalSkinIndex == nil
or self.vehicle:getSkinIndex() ~= self.originalSkinIndex then
return false
end
local requirements = PaintMyKI5.PaintRequirements.getRequirements(
self.vehicle, self.skinIndex
)
if not requirements then return false end
local enough = PaintMyKI5.PaintInventory.checkRequirements(
self.character, requirements
)
if not enough then return false end
local vehicle = self.vehicle
local changed = pcall(applySkin, vehicle, self.skinIndex)
if not changed then
pcall(applySkin, vehicle, self.originalSkinIndex)
return false
end
if not PaintMyKI5.PaintInventory.consumeRequirements(self.character, requirements) then
pcall(applySkin, vehicle, self.originalSkinIndex)
return false
end
return true
end
function ISPaintMyKI5Vehicle:getDuration()
if self.character:isTimedActionInstant() then return 1 end
return 500
end
function ISPaintMyKI5Vehicle:new(character, vehicle, skinIndex, area)
local action = ISBaseTimedAction.new(self, character)
action.vehicle = vehicle
action.skinIndex = skinIndex
action.area = area
action.originalSkinIndex = vehicle and vehicle:getSkinIndex() or nil
action.maxTime = action:getDuration()
action.stopOnWalk = true
action.stopOnRun = true
action.caloriesModifier = 4
return action
end
@@ -0,0 +1,3 @@
{
"ContextMenu_PaintMyKI5": "Paint vehicle"
}
@@ -0,0 +1,9 @@
{
"IGUI_PaintMyKI5_Title": "Paint KI5 Vehicle",
"IGUI_PaintMyKI5_Skin": "Skin %1",
"IGUI_PaintMyKI5_Paint": "Paint",
"IGUI_PaintMyKI5_Requirements": "Paint requirements",
"IGUI_PaintMyKI5_RequirementLine": "%1: %2%%, %3 uses needed, %4 available",
"IGUI_PaintMyKI5_OneBucket": "Total: %1 uses — one full bucket",
"IGUI_PaintMyKI5_PreviewMissing": "Texture preview unavailable"
}