-- vim: set foldmethod=marker ts=4 sw=4 : -- TODO rewrite variables: armor_model, model --- Initial definitions --- -- player model backwards compatibility model=models.player_model armor_model={ ["BOOTS"]=vanilla_model.BOOTS, ["LEGGINGS"]=vanilla_model.LEGGINGS, ["CHESTPLATE"]=vanilla_model.CHESTPLATE, ["HELMET"]=vanilla_model.HELMET } ping=pings -- Texture dimensions -- TEXTURE_WIDTH = 256 TEXTURE_HEIGHT = 256 util = require("nulllib.util") logging = require("nulllib.logging") -- Timer (not mine lol) -- {{{ -- TODO investigate if events can replace some of this do local timers = {} function wait(ticks,next) table.insert(timers, {t=world.getTime()+ticks,n=next}) end local function tick() for key,timer in pairs(timers) do if world.getTime() >= timer.t then timer.n() table.remove(timers,key) end end end events.TICK:register(function() if player then tick() end end, "timer") end -- named timers (this one is mine but heavily based on the other) -- -- if timer is armed twice before expiring it will only be called once) -- do local timers = {} function namedWait(ticks, next, name) -- main difference, this will overwrite an existing timer with -- the same name timers[name]={t=world.getTime()+ticks,n=next} end local function tick() for key, timer in pairs(timers) do if world.getTime() >= timer.t then timer.n() timers[key]=nil end end end events.TICK:register(function() if player then tick() end end, "named_timer") end -- named cooldowns do local timers={} function cooldown(ticks, name) if timers[name] == nil then timers[name]={t=world.getTime()+ticks} return true end return false end local function tick() for key, timer in pairs(timers) do if world.getTime() >= timer.t then timers[key]=nil end end end events.TICK:register(function() if player then tick() end end, "cooldown") end function rateLimit(ticks, next, name) if cooldown(ticks+1, name) then namedWait(ticks, next, name) end end -- }}} -- syncState {{{ do local counter=0 function syncState() -- ping.setSnoring(skin_state.snore_enabled) if counter < 3 then ping.syncState((setLocalState())) counter=counter+1 end end local function cooldownDecay() if counter>0 and world.getTime() % 4 == 0 then counter = counter - 1 end end events.TICK:register(cooldownDecay,"syncStateCooldown") end do local pm_refresh=false function pmRefresh() pm_refresh=true end function doPmRefresh() if pm_refresh then PartsManager.refreshAll() pm_refresh=false end end end function ping.syncState(tbl) logging.debug("ping.syncState") for k, v in pairs(tbl) do local_state[k]=v end pmRefresh() end -- }}} -- math functions nmath=require("nulllib.math") lerp=math.lerp -- this is implemented in figura now wave=nmath.wave -- Master and local state variables -- {{{ -- Local State (these are copied by pings at runtime) -- local_state={} old_state={} -- master state variables and configuration (do not access within pings) -- do local is_host=host:isHost() local defaults={ ["armor_enabled"]=true, ["vanilla_enabled"]=false, ["snore_enabled"]=true, ["print_settings"]=false, ["vanilla_partial"]=false, ["tail_enabled"]=true, ["aquatic_enabled"]=true, ["aquatic_override"]=false } function setLocalState() if is_host then for k, v in pairs(skin_state) do local_state[k]=v end else for k, v in pairs(defaults) do if local_state[k] == nil then local_state[k]=v end end end return local_state end -- TODO reimplement with new data API -- if is_host then if false then local savedData=data.loadAll() if savedData == nil then for k, v in pairs(defaults) do data.save(k, v) end savedData=data.loadAll() end skin_state=util.mergeTable( util.map(util.parse,data.loadAll()), defaults) else skin_state=defaults end setLocalState() end function printSettings() print("Settings:") for k, v in pairs(skin_state) do print(tostring(k)..": "..tostring(v)) end end if skin_state.print_settings==true then printSettings() end function setState(name, state) if state == nil then skin_state[name]=not skin_state[name] else skin_state[name]=state end -- TODO -- data.save(name, skin_state[name]) end -- }}} PartsManager=require("nulllib.PartsManager") -- TODO: accept model part for built-in UV management, automatic texture size UVManager=require("nulllib.UVManager") -- Parts, groups, other constants -- {{{ HEAD=model.Head.Head FACE=model.Head.Face SHATTER=model.Head.Shatter VANILLA_PARTIAL={} VANILLA_GROUPS={ ["HEAD"]={vanilla_model.HEAD, vanilla_model.HAT}, ["BODY"]={vanilla_model.BODY, vanilla_model.JACKET}, ["LEFT_ARM"]={vanilla_model.LEFT_ARM, vanilla_model.LEFT_SLEEVE}, ["RIGHT_ARM"]={vanilla_model.RIGHT_ARM, vanilla_model.RIGHT_SLEEVE}, ["LEFT_LEG"]={vanilla_model.LEFT_LEG, vanilla_model.LEFT_PANTS_LEG}, ["RIGHT_LEG"]={vanilla_model.RIGHT_LEG, vanilla_model.RIGHT_PANTS_LEG}, ["OUTER"]={ vanilla_model.HAT, vanilla_model.JACKET, vanilla_model.LEFT_SLEEVE, vanilla_model.RIGHT_SLEEVE, vanilla_model.LEFT_PANTS_LEG, vanilla_model.RIGHT_PANTS_LEG }, ["INNER"]={ vanilla_model.HEAD, vanilla_model.BODY, vanilla_model.LEFT_ARM, vanilla_model.RIGHT_ARM, vanilla_model.LEFT_LEG, vanilla_model.RIGHT_LEG }, ["ALL"]={ vanilla_model.HEAD, vanilla_model.BODY, vanilla_model.LEFT_ARM, vanilla_model.RIGHT_ARM, vanilla_model.LEFT_LEG, vanilla_model.RIGHT_LEG, vanilla_model.HAT, vanilla_model.JACKET, vanilla_model.LEFT_SLEEVE, vanilla_model.RIGHT_SLEEVE, vanilla_model.LEFT_PANTS_LEG, vanilla_model.RIGHT_PANTS_LEG }, ["ARMOR"]=armor_model } -- these are inefficient, redundancy is better in this case -- for _, v in pairs(VANILLA_GROUPS.INNER) do table.insert(VANILLA_GROUPS.ALL,v) end -- for _, v in pairs(VANILLA_GROUPS.OUTER) do table.insert(VANILLA_GROUPS.ALL,v) end -- for _, v in pairs(armor_model) do table.insert(VANILLA_GROUPS.ARMOR, v) end MAIN_GROUPS={model.Head, model.RightArm, model.LeftArm, model.RightLeg, model.LeftLeg, model.Body } -- RightArm LeftArm RightLeg LeftLeg Body Head TAIL_LEGGINGS={ model.Body.LeggingsTop, model.Body.LeggingsTopTrimF, model.Body.LeggingsTopTrimB, model.Body.MTail1.Leggings, model.Body.MTail1.LeggingsTrim, model.Body.MTail1.MTail2.LeggingsBottom } TAIL_LEGGINGS_COLOR={ model.Body.LeggingsTopTrimF, model.Body.LeggingsTopTrimB, model.Body.MTail1.Leggings, model.Body.MTail1.LeggingsTrim, model.Body.MTail1.MTail2.LeggingsBottom } TAIL_BOOTS={ model.Body.MTail1.MTail2.MTail3.Boot, model.Body.MTail1.MTail2.MTail3.LeatherBoot } TAIL_BONES={ model.Body.MTail1, model.Body.MTail1.MTail2, model.Body.MTail1.MTail2.MTail3, model.Body.MTail1.MTail2.MTail3.MTail4 } REG_TAIL_BONES={ model.Body_Tail, model.Body_Tail.Tail_L2, model.Body_Tail.Tail_L2.Tail_L3, model.Body_Tail.Tail_L2.Tail_L3.fin } BODY_EMISSIVES={ model.Body.MTail1.MTailDots1, model.Body.MTail1.MTail2.MTailDots2, model.Body.MTail1.MTail2.MTail3.MTailDots3, model.Body.MTail1.MTail2.MTail3.MTail4.MTailDots4, model.Body_Tail.TailDots1, model.Body_Tail.Tail_L2.TailDots2, model.Body_Tail.Tail_L2.Tail_L3.TailDots3, model.Body_Tail.Tail_L2.Tail_L3.fin.TailDots4, model.Head.EmDots, model.LeftArm.LeftArmEm, model.RightArm.RightArmEm, model.LeftLeg.LeftLegEm, model.RightLeg.RightLegEm } FACE_EMISSIVES={ model.Head.Face } EMISSIVES=util.mergeTable(BODY_EMISSIVES, FACE_EMISSIVES) COLORS={} COLORS.neutral=vec(127/255,127/255,255/255) COLORS.hurt= vec(1, 0, 63/255) COLORS.lava= vec(1, 128/255, 64/255) -- prev 255 160 192 COLORS.owo= vec(1, 128/255, 160/255) COLORS["end"]="end" for k, v in pairs(EMISSIVES) do v:setColor(COLORS.neutral) end -- }}} -- PartsManager rules {{{ -- Vanilla rules do -- TODO function getVanillaVisible() return (not avatar:canEditVanillaModel()) or vanilla_model.PLAYER:getVisible() end local function vanillaPartial() if local_state.vanilla_enabled then return false end return local_state.vanilla_partial end local function forceVanilla() print(vanilla_model.PLAYER:getVisible()) return not avatar:canEditVanillaModel() or local_state.vanilla_enabled or vanilla_model.PLAYER:getVisible() end -- eventually replace this with an instance once PartsManager becomes a class local PM=PartsManager --- Vanilla state -- no cape if tail enabled (it clips) PM.addPartFunction(vanilla_model.CAPE, function(last) return last and not local_state.tail_enabled end) --- Custom state -- local tail_parts=util.mergeTable({model.Body.TailBase}, TAIL_BONES) local tail_parts={model.Body.MTail1, model.Body.TailBase} -- TODO: old vanilla_partial groups, use these for texture swap -- local vanilla_partial_disabled=util.mergeTable(MAIN_GROUPS, {model.Body.Body, model.Body.BodyLayer}) -- local vanilla_partial_enabled={model.Head, model.Body} -- Show shattered only at low health PM.addPartFunction(SHATTER, function(last) return last and local_state.health <= 5 end) -- Enable tail setting PM.addPartFunction(model.Body_Tail, function(last) return last and local_state.tail_enabled end) -- no legs, regular tail in water if tail enabled local mtail_mutually_exclusive={model.LeftLeg, model.RightLeg, model.Body_Tail, armor_model.LEGGINGS, armor_model.BOOTS} PM.addPartGroupFunction(mtail_mutually_exclusive, function(last) return last and not aquaticTailVisible() end) -- aquatic tail in water PM.addPartGroupFunction(tail_parts, function(last) return last and aquaticTailVisible() end) --- Armor state local all_armor=util.reduce(util.mergeTable, {VANILLA_GROUPS.ARMOR, TAIL_LEGGINGS, TAIL_BOOTS}) PM.addPartGroupFunction(all_armor, function(last) return last and local_state.armor_enabled end) -- Only show armor if equipped PM.addPartFunction(model.Body.MTail1.MTail2.MTail3.Boot, function(last) return last and armor_state.boots end) PM.addPartFunction(model.Body.MTail1.MTail2.MTail3.LeatherBoot, function(last) return last and armor_state.leather_boots end) PM.addPartGroupFunction(TAIL_LEGGINGS, function(last) return last and armor_state.leggings end) -- Disable when vanilla_enabled PM.addPartGroupFunction(MAIN_GROUPS, function(last) return last and not getVanillaVisible() end) end SNORES={"snore-1", "snore-2", "snore-3"} -- }}} -- Expression change -- {{{ do local expressions={} expressions.neutral=vec(0,0) expressions["end"]=expressions.neutral expressions.hurt=vec(0,1) expressions.owo=vec(0,2) local expruvm=UVManager:new(vec(8, 8), nil, expressions, FACE) current_expression="neutral" -- color/expression rules function getBestColor() if current_expression=="owo" then return COLORS.owo elseif player:isInLava() or player:getDimensionName()=="minecraft:the_nether" then return COLORS.lava else return COLORS.neutral end end function getBestExpression() return "neutral" end function setColor(col) if not lock_color then col=(col~=nil) and col or getBestColor() for _, v in pairs(EMISSIVES) do v:setColor(col) -- TODO -- v:setShader("None") end end end -- Expression change code function setExpression(expression) current_expression=expression expruvm:setUV(current_expression) -- This expression sticks, so do not set color explicitly setColor() end function changeExpression(expression, ticks) expruvm:setUV(expression) -- This one is for more explicit "flashes" such as player hurt -- animations, get color explicitly setColor(COLORS[expression]) namedWait(ticks, resetExpression, "resetExpression") end function resetExpression() lock_color=false expruvm:setUV(current_expression) setColor() end function hurt() lock_color=false changeExpression("hurt", 10) lock_color=true PartsManager.refreshPart(SHATTER) end end -- }}} -- Action Wheel & Pings -- {{{ -- TODO do wheel={} local wheel_index=1 function wheelScroll(change) wheel_index=((wheel_index-1)+change)%#wheel+1 action_wheel:setPage(wheel[wheel_index]) print("page: " .. wheel_index) end wheel[1]=action_wheel:newPage() action_wheel:setPage(wheel[1]) action_wheel.scroll=wheelScroll end wheel[1]:newAction():title('test expression'):onLeftClick(function() ping.expressionTest() end) function ping.expressionTest() logging.debug("ping.expressionTest") changeExpression("hurt", 10) end wheel[1]:newAction():title('log health'):onLeftClick(function() print(player:getHealth()) end) wheel[1]:newAction():title('Toggle Armor'):onLeftClick(function() setArmor() end) wheel[1]:newAction():title('T-Pose'):onLeftClick(function() ping.tPose() end) wheel[1]:newAction():title('UwU'):onLeftClick(function() ping.expr("owo") end) -- action_wheel.SLOT_8.setTitle('sssss...') -- action_wheel.SLOT_8.setItem("minecraft:creeper_head") -- action_wheel.SLOT_8.setFunction(function() switch_model('misc/Creeper') end) -- Pings -- --- Damage function -- function ping.expr(expr) logging.debug("ping.expr") local val=(expr==current_expression) and "neutral" or expr setExpression(val) end function ping.oof(health) -- This is a replacement for onDamage, that function doesn't sync for some reason logging.debug("ping.oof") hurt() end --- Toggle Armor --- function setArmor(state) setState("armor_enabled", state) syncState() end function snore() end -- TODO re-enable snoring -- do -- local snore_enabled=false -- local snore_index=1 -- function snore() -- if snore_enabled then -- -- TODO -- -- sound.playCustomSound(SNORES[snore_index], -- -- player.getPos(), vectors.of{20,1}) -- snore_index=snore_index%#SNORES+1 -- end -- end -- -- function setSnoring(state) -- setState("snore_enabled", state) -- ping.setSnoring(skin_state.snore_enabled) -- end -- -- function ping.setSnoring(state) -- snore_enabled=state -- end -- end --- Toggle Vanilla --- function setVanilla(state) setState("vanilla_enabled", state) syncState() end function ping.tPose() logging.debug("ping.tPose") local_state.emote_vector=player:getPos() -- TODO -- animation.tpose.start() end -- }}} -- Tail stuff {{{ function aquaticTailVisible() tail_cooldown=tail_cooldown or 0 return (local_state.aquatic_enabled and (player:isInWater() or player:isInLava()) or local_state.aquatic_override or tail_cooldown>0) and not getVanillaVisible() end function updateTailVisibility() local anim=player:getPose() local water=player:isInWater() local lava=player:isInLava() tail_cooldown=(tail_cooldown and tail_cooldown > 0) and tail_cooldown-1 or 0 if aquaticTailVisible() and (anim=="SLEEPING" or anim=="SPIN_ATTACK" or anim=="FALL_FLYING" or water or lava) then tail_cooldown=anim=="SPIN_ATTACK" and 60 or (tail_cooldown >= 10 and tail_cooldown or 10) end if old_state.aquaticTailVisible ~= aquaticTailVisible() then pmRefresh() end old_state.aquaticTailVisible=aquaticTailVisible() end -- armor {{{ armor_color={} armor_color['leather'] = {131 /255 , 84 /255 , 50 /255} armor_glint={} armor_state={} armor_state['leggings']=false armor_state['boots']=false armor_state['leather_boots']=false do local positions={} positions['leather']=vec(0, 0) positions['iron']=vec(0, 1) positions['chainmail']=vec(0, 2) positions['golden']=vec(0, 3) positions['diamond']=vec(0, 4) positions['netherite']=vec(0, 5) tailuvm=UVManager:new(vec(0, 19), nil, positions) end -- TODO fix code after optimization in prewrite function armor() if true then return nil end -- ^ hacky way to disable a function without uncommenting the entire thing to not break git vcs -- Get equipped armor, extract name from item ID local leggings_item = player.getEquipmentItem(4) local boots_item = player.getEquipmentItem(3) local leggings = string.sub(leggings_item.getType(), 11, -10) local boots = string.sub(boots_item.getType(), 11, -7) if local_state.armor_enabled then if old_state.leggings ~= leggings or old_state.armor_enabled ~= local_state.armor_enabled then -- leggings armor_glint.leggings=leggings_item.hasGlint() local leggings_color=colorArmor(leggings_item) or armor_color[leggings] local uv=tailuvm:getUV(leggings) if uv ~= nil then armor_state.leggings=true for k, v in pairs(TAIL_LEGGINGS) do v.setUV(uv) end if leggings=="leather" then for k, v in pairs(TAIL_LEGGINGS_COLOR) do v.setColor(leggings_color) end else for k, v in pairs(TAIL_LEGGINGS) do v.setColor({1, 1, 1}) end end else armor_state.leggings=false end pmRefresh() end if old_state.boots ~= boots or old_state.armor_enabled ~= local_state.armor_enabled then -- boots armor_glint.boots=boots_item.hasGlint() local boots_color=colorArmor(boots_item) or armor_color[boots] local uv_boots=tailuvm:getUV(boots) if uv_boots ~= nil then armor_state.boots=true for k, v in pairs(TAIL_BOOTS) do v.setUV(uv_boots) end if boots=="leather" then model.Body.MTail1.MTail2.MTail3.Boot.setColor(boots_color) armor_state.leather_boots=true else model.Body.MTail1.MTail2.MTail3.Boot.setColor({1, 1, 1}) armor_state.leather_boots=false end else armor_state.boots=false end pmRefresh() end else armor_glint.leggings=false armor_glint.boots=false end if armor_glint.leggings then for _, v in pairs(TAIL_LEGGINGS) do v.setShader("Glint") end else for _, v in pairs(TAIL_LEGGINGS) do v.setShader("None") end end if armor_glint.boots then for _, v in pairs(TAIL_BOOTS) do v.setShader("Glint") end else for _, v in pairs(TAIL_BOOTS) do v.setShader("None") end end old_state.boots=boots old_state.leggings=leggings old_state.armor_enabled=local_state.armor_enabled end function colorArmor(item) local tag = item.tag if tag ~= nil and tag.display ~= nil and tag.display.color ~= nil then return vectors.intToRGB(tag.display.color) end end -- }}} function resetAngles(part) part:setRot(vec(0,0,0)) end function animateMTail(val) local chest_rot = 3 local per=2*math.pi model.Body:setRot(vec( wave(val, per, 3), 0, 0 )) -- TODO vanilla model manipulation broke, add chestplate model -- armor_model.CHESTPLATE:setRot(vec( -wave(val, per, math.rad(3)), 0, 0 )) -- this makes it work with partial vanilla -- vanilla_model.BODY:setRot(vec( -wave(val, per, math.rad(3)), 0, 0 )) -- vanilla_model.JACKET:setRot(vec( -wave(val, per, math.rad(3)), 0, 0 )) model.Body.LeggingsTopTrimF:setRot(vec( wave(val-1, per, 4), 0, 0 )) model.Body.LeggingsTopTrimB:setRot(vec( wave(val-1, per, 4), 0, 0 )) TAIL_BONES[1]:setRot(vec( wave(val-1, per, 7), 0, 0 )) TAIL_BONES[2]:setRot(vec( wave(val-2, per, 8), 0, 0 )) TAIL_BONES[3]:setRot(vec( wave(val-3, per, 12), 0, 0 )) TAIL_BONES[4]:setRot(vec( wave(val-4, per, 15), 0, 0 )) end tail_original_rot={} for k, v in ipairs(REG_TAIL_BONES) do tail_original_rot[k]=v:getRot() end function animateTail(val) local per_y=20*4 local per_x=20*6 for k, v in ipairs(REG_TAIL_BONES) do local cascade=(k-1)*12 REG_TAIL_BONES[k]:setRot(vec( tail_original_rot[k].x + wave(val-cascade, per_x, 3), wave(val-cascade, per_y, 12), tail_original_rot[k].z )) end end anim_tick=0 anim_cycle=0 old_state.anim_cycle=0 function animateTick() anim_tick = anim_tick + 1 if aquaticTailVisible() then local velocity = player:getVelocity() if aquaticTailVisible() then old_state.anim_cycle=anim_cycle local player_speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2) local animation=player:getPose() local factor=(not player:isInWater() and (animation=="FALL_FLYING" or animation=="SPIN_ATTACK")) and 0.5 or 5 anim_cycle=anim_cycle + (player_speed*factor+0.75) -- bubble animation would go here but i don't have that (yet) end else old_state.anim_cycle=anim_cycle anim_cycle=anim_cycle+1 end end -- }}} -- initialize values -- {{{ function player_init() local_state.health=player:getHealth() old_state.health=local_state.health -- TODO possibly reconsider if this should be redone -- actually it's probably fine, it's jsut here because i forget visibility settings -- local all_parts=util.recurseModelGroup(model) -- for k, v in pairs(all_parts) do -- v:setVisible(nil) -- end setLocalState() pmRefresh() syncState() events.ENTITY_INIT:remove("player_init") end events.ENTITY_INIT:register(function() return player_init() end, "player_init") -- Initial configuration -- -- TODO x2 fix below, this entire block may not be needed with PartsManager if avatar:canEditVanillaModel() then vanilla_model.PLAYER:setVisible(false) else model:setVisible(false) end anim_tick=0 -- }}} -- Tick function -- {{{ function hostTick() local_state.health=player:getHealth() if local_state.health ~= old_state.health then if local_state.health < old_state.health then ping.oof(local_state.health) end syncState() end end function tick() color_check=player:isInLava() ~= (player:getDimensionName()=="minecraft:the_nether") if old_state.color_check~=color_check then setColor() end -- optimization, only execute these once a second -- if world.getTimeOfDay() % 20 == 0 then if player:getPose() == "SLEEPING" then if cooldown(20*4, "snore") then snore() end end -- Sync state every 10 seconds if world.getTimeOfDay() % (20*10) == 0 then syncState() end end hostTick() -- TODO -- if animation.tpose.isPlaying() and local_state.emote_vector.distanceTo(player.getPos()) >= 0.5 then -- animation.tpose.stop() -- end -- Refresh tail armor state armor() -- Implements tail cooldown conditions updateTailVisibility() -- Animation code resides in this function animateTick() -- Check for queued PartsManager refresh doPmRefresh() -- End of tick -- old_state.health=player:getHealth() old_state.color_check=color_check local_state.anim=player:getPose() end events.TICK:register(function() if player then tick() end end, "main_tick") -- }}} -- Render function {{{ function render(delta) if aquaticTailVisible() then animateMTail((lerp(old_state.anim_cycle, anim_cycle, delta) * 0.2)) else resetAngles(model.Body) -- resetAngles(vanilla_model.BODY) -- resetAngles(vanilla_model.JACKET) -- resetAngles(armor_model.CHESTPLATE) animateTail((lerp(old_state.anim_cycle, anim_cycle, delta))) end end -- TODO this may break animation during death events.RENDER:register(function(delta) if player then render(delta) end end, "main_render") -- }}}