499 lines
20 KiB
Lua
499 lines
20 KiB
Lua
-- Convenient functions for Urho2D samples:
|
|
-- - Generate collision shapes from a tmx file objects
|
|
-- - Create Spriter Imp character
|
|
-- - Load Mover script object class from file
|
|
-- - Create enemies, coins and platforms to tile map placeholders
|
|
-- - Handle camera zoom using PageUp, PageDown and MouseWheel
|
|
-- - Create UI interface
|
|
-- - Create a particle emitter attached to a given node
|
|
-- - Play a non-looping sound effect
|
|
-- - Create a background sprite
|
|
-- - Set global variables
|
|
-- - Set XML patch instructions for screen joystick
|
|
|
|
CAMERA_MIN_DIST = 0.1
|
|
CAMERA_MAX_DIST = 6
|
|
|
|
MOVE_SPEED = 23 -- Movement speed as world units per second
|
|
MOVE_SPEED_X = 2.5 -- Movement speed for isometric maps
|
|
MOVE_SPEED_SCALE = 1 -- Scaling factor based on tiles' aspect ratio
|
|
|
|
LIFES = 3
|
|
zoom = 2 -- Speed is scaled according to zoom
|
|
demoFilename = ""
|
|
character2DNode = nil
|
|
|
|
|
|
function CreateCollisionShapesFromTMXObjects(tileMapNode, tileMapLayer, info)
|
|
-- Create rigid body to the root node
|
|
local body = tileMapNode:CreateComponent("RigidBody2D")
|
|
body.bodyType = BT_STATIC
|
|
|
|
-- Generate physics collision shapes from the tmx file's objects located in "Physics" layer
|
|
for i=0, tileMapLayer:GetNumObjects() -1 do
|
|
local tileMapObject = tileMapLayer:GetObject(i) -- Get physics objects (TileMapObject2D)
|
|
local objectType = tileMapObject.objectType
|
|
|
|
-- Create collision shape from tmx object
|
|
local shape
|
|
|
|
if objectType == OT_RECTANGLE then
|
|
shape = tileMapNode:CreateComponent("CollisionBox2D")
|
|
local size = tileMapObject.size
|
|
shape.size = size
|
|
if info.orientation == O_ORTHOGONAL then
|
|
shape.center = tileMapObject.position + size / 2
|
|
else
|
|
shape.center = tileMapObject.position + Vector2(info.tileWidth / 2, 0)
|
|
shape.angle = 45 -- If our tile map is isometric then shape is losange
|
|
end
|
|
|
|
elseif objectType == OT_ELLIPSE then
|
|
shape = tileMapNode:CreateComponent("CollisionCircle2D") -- Ellipse is built as a circle shape as there's no equivalent in Box2D
|
|
local size = tileMapObject.size
|
|
shape.radius = size.x / 2
|
|
if info.orientation == O_ORTHOGONAL then
|
|
shape.center = tileMapObject.position + size / 2
|
|
else
|
|
shape.center = tileMapObject.position + Vector2(info.tileWidth / 2, 0)
|
|
end
|
|
|
|
elseif objectType == OT_POLYGON then
|
|
shape = tileMapNode:CreateComponent("CollisionPolygon2D")
|
|
|
|
elseif objectType == OT_POLYLINE then
|
|
shape = tileMapNode:CreateComponent("CollisionChain2D")
|
|
|
|
else break
|
|
end
|
|
|
|
if objectType == OT_POLYGON or objectType == OT_POLYLINE then -- Build shape from vertices
|
|
local numVertices = tileMapObject.numPoints
|
|
shape.vertexCount = numVertices
|
|
for i=0, numVertices - 1 do
|
|
shape:SetVertex(i, tileMapObject:GetPoint(i))
|
|
end
|
|
end
|
|
|
|
shape.friction = 0.8
|
|
if tileMapObject:HasProperty("Friction") then
|
|
shape.friction = ToFloat(tileMapObject:GetProperty("Friction"))
|
|
end
|
|
end
|
|
end
|
|
|
|
function CreateCharacter(info, createObject, friction, position, scale)
|
|
character2DNode = scene_:CreateChild("Imp")
|
|
character2DNode.position = position
|
|
character2DNode:SetScale(scale)
|
|
local animatedSprite = character2DNode:CreateComponent("AnimatedSprite2D")
|
|
local animationSet = cache:GetResource("AnimationSet2D", "Urho2D/imp/imp.scml")
|
|
animatedSprite.animationSet = animationSet
|
|
animatedSprite.animation = "idle"
|
|
animatedSprite:SetLayer(3) -- Put character over tile map (which is on layer 0) and over Orcs (which are on layer 1)
|
|
--
|
|
local body = character2DNode:CreateComponent("RigidBody2D")
|
|
body.bodyType = BT_DYNAMIC
|
|
body.allowSleep = false
|
|
|
|
local shape = character2DNode:CreateComponent("CollisionCircle2D")
|
|
shape.radius = 1.1 -- Set shape size
|
|
shape.friction = friction -- Set friction
|
|
shape.restitution = 0.1 -- Slight bounce
|
|
if createObject then
|
|
character2DNode:CreateScriptObject("Character2D") -- Create a ScriptObject to handle character behavior
|
|
end
|
|
|
|
-- Scale character's speed on the Y axis according to tiles' aspect ratio (for isometric only)
|
|
MOVE_SPEED_SCALE = info.tileHeight / info.tileWidth
|
|
end
|
|
|
|
function CreateTrigger()
|
|
local node = scene_:CreateChild("Trigger") -- Clones will be renamed according to object type
|
|
local body = node:CreateComponent("RigidBody2D")
|
|
body.bodyType = BT_STATIC
|
|
local shape = node:CreateComponent("CollisionBox2D") -- Create box shape
|
|
shape.trigger = true
|
|
return node
|
|
end
|
|
|
|
function CreateEnemy()
|
|
local node = scene_:CreateChild("Enemy")
|
|
local staticSprite = node:CreateComponent("StaticSprite2D")
|
|
staticSprite.sprite = cache:GetResource("Sprite2D", "Urho2D/Aster.png")
|
|
local body = node:CreateComponent("RigidBody2D")
|
|
body.bodyType = BT_STATIC
|
|
local shape = node:CreateComponent("CollisionCircle2D") -- Create circle shape
|
|
shape.radius = 0.25 -- Set radius
|
|
return node
|
|
end
|
|
|
|
function CreateOrc()
|
|
local node = scene_:CreateChild("Orc")
|
|
node.scale = character2DNode.scale -- Use same scale as player character
|
|
local animatedSprite = node:CreateComponent("AnimatedSprite2D")
|
|
-- Get scml file and Play "run" anim
|
|
local animationSet = cache:GetResource("AnimationSet2D", "Urho2D/Orc/Orc.scml")
|
|
animatedSprite.animationSet = animationSet
|
|
animatedSprite.animation = "run"
|
|
animatedSprite:SetLayer(2) -- Make orc always visible
|
|
local body = node:CreateComponent("RigidBody2D")
|
|
local shape = node:CreateComponent("CollisionCircle2D") -- Create circle shape
|
|
shape.radius = 1.3 -- Set shape size
|
|
shape.trigger = true
|
|
return node
|
|
end
|
|
|
|
function CreateCoin()
|
|
local node = scene_:CreateChild("Coin")
|
|
node:SetScale(0.5)
|
|
local animatedSprite = node:CreateComponent("AnimatedSprite2D")
|
|
-- Get scml file and Play "idle" anim
|
|
local animationSet = cache:GetResource("AnimationSet2D", "Urho2D/GoldIcon.scml")
|
|
animatedSprite.animationSet = animationSet
|
|
animatedSprite.animation = "idle"
|
|
animatedSprite:SetLayer(2)
|
|
local body = node:CreateComponent("RigidBody2D")
|
|
body.bodyType = BT_STATIC
|
|
local shape = node:CreateComponent("CollisionCircle2D") -- Create circle shape
|
|
shape.radius = 0.32 -- Set radius
|
|
shape.trigger = true
|
|
return node
|
|
end
|
|
|
|
function CreateMovingPlatform()
|
|
local node = scene_:CreateChild("MovingPlatform")
|
|
node.scale = Vector3(3, 1, 0)
|
|
local staticSprite = node:CreateComponent("StaticSprite2D")
|
|
staticSprite.sprite = cache:GetResource("Sprite2D", "Urho2D/Box.png")
|
|
local body = node:CreateComponent("RigidBody2D")
|
|
body.bodyType = BT_STATIC
|
|
local shape = node:CreateComponent("CollisionBox2D") -- Create box shape
|
|
shape.size = Vector2(0.32, 0.32) -- Set box size
|
|
shape.friction = 0.8 -- Set friction
|
|
return node
|
|
end
|
|
|
|
function PopulateMovingEntities(movingEntitiesLayer)
|
|
-- Create enemy, Orc and moving platform nodes (will be cloned at each placeholder)
|
|
local enemyNode = CreateEnemy()
|
|
local orcNode = CreateOrc()
|
|
local platformNode = CreateMovingPlatform()
|
|
|
|
-- Instantiate enemies and moving platforms at each placeholder (placeholders are Poly Line objects defining a path from points)
|
|
for i=0, movingEntitiesLayer:GetNumObjects() -1 do
|
|
-- Get placeholder object (TileMapObject2D)
|
|
local movingObject = movingEntitiesLayer:GetObject(i)
|
|
if movingObject.objectType == OT_POLYLINE then
|
|
-- Clone the moving entity node and position it at placeholder point
|
|
local movingClone = nil
|
|
local offset = Vector2.ZERO
|
|
if movingObject.type == "Enemy" then
|
|
movingClone = enemyNode:Clone()
|
|
offset = Vector2(0, -0.32)
|
|
elseif movingObject.type == "Orc" then
|
|
movingClone = orcNode:Clone()
|
|
elseif movingObject.type == "MovingPlatform" then
|
|
movingClone = platformNode:Clone()
|
|
else
|
|
break
|
|
end
|
|
movingClone.position2D = movingObject:GetPoint(0) + offset
|
|
|
|
-- Create script object that handles entity translation along its path (load from file)
|
|
local mover = movingClone:CreateScriptObject("LuaScripts/Utilities/2D/Mover.lua", "Mover")
|
|
|
|
-- Set path from points
|
|
mover.path = CreatePathFromPoints(movingObject, offset)
|
|
|
|
-- Override default speed
|
|
if movingObject:HasProperty("Speed") then
|
|
mover.speed = movingObject:GetProperty("Speed")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Remove nodes used for cloning purpose
|
|
enemyNode:Remove()
|
|
orcNode:Remove()
|
|
platformNode:Remove()
|
|
end
|
|
|
|
function PopulateCoins(coinsLayer)
|
|
-- Create coin (will be cloned at each placeholder)
|
|
local coinNode = CreateCoin()
|
|
|
|
-- Instantiate coins to pick at each placeholder
|
|
for i=0, coinsLayer:GetNumObjects() -1 do
|
|
local coinObject = coinsLayer:GetObject(i) -- Get placeholder object (TileMapObject2D)
|
|
local coinClone = coinNode:Clone()
|
|
coinClone.position2D = coinObject.position + coinObject.size / 2 + Vector2(0, 0.16)
|
|
end
|
|
|
|
-- Init coins counters
|
|
local character = character2DNode:GetScriptObject()
|
|
character.remainingCoins = coinsLayer.numObjects
|
|
character.maxCoins = coinsLayer.numObjects
|
|
|
|
-- Remove node used for cloning purpose
|
|
coinNode:Remove()
|
|
end
|
|
|
|
function PopulateTriggers(triggersLayer)
|
|
-- Create trigger node (will be cloned at each placeholder)
|
|
local triggerNode = CreateTrigger()
|
|
|
|
-- Instantiate triggers at each placeholder (Rectangle objects)
|
|
for i=0, triggersLayer:GetNumObjects() -1 do
|
|
local triggerObject = triggersLayer:GetObject(i) -- Get placeholder object (TileMapObject2D)
|
|
if triggerObject.objectType == OT_RECTANGLE then
|
|
local triggerClone = triggerNode:Clone()
|
|
triggerClone.name = triggerObject.type
|
|
triggerClone:GetComponent("CollisionBox2D").size = triggerObject.size
|
|
triggerClone.position2D = triggerObject.position + triggerObject.size / 2
|
|
end
|
|
end
|
|
end
|
|
|
|
function Zoom(camera)
|
|
if input.mouseMoveWheel then
|
|
zoom = Clamp(camera.zoom + input.mouseMoveWheel * 0.1, CAMERA_MIN_DIST, CAMERA_MAX_DIST)
|
|
camera.zoom = zoom
|
|
end
|
|
|
|
if input:GetKeyDown(KEY_PAGEUP) then
|
|
zoom = Clamp(camera.zoom * 1.01, CAMERA_MIN_DIST, CAMERA_MAX_DIST)
|
|
camera.zoom = zoom
|
|
end
|
|
|
|
if input:GetKeyDown(KEY_PAGEDOWN) then
|
|
zoom = Clamp(camera.zoom * 0.99, CAMERA_MIN_DIST, CAMERA_MAX_DIST)
|
|
camera.zoom = zoom
|
|
end
|
|
end
|
|
|
|
function CreatePathFromPoints(object, offset)
|
|
local path = {}
|
|
for i=0, object.numPoints -1 do
|
|
table.insert(path, object:GetPoint(i) + offset)
|
|
end
|
|
return path
|
|
end
|
|
|
|
function CreateUIContent(demoTitle)
|
|
-- Set the default UI style and font
|
|
ui.root.defaultStyle = cache:GetResource("XMLFile", "UI/DefaultStyle.xml")
|
|
local font = cache:GetResource("Font", "Fonts/Anonymous Pro.ttf")
|
|
|
|
-- We create in-game UIs (coins and lifes) first so that they are hidden by the fullscreen UI (we could also temporary hide them using SetVisible)
|
|
|
|
-- Create the UI for displaying the remaining coins
|
|
local coinsUI = ui.root:CreateChild("BorderImage", "Coins")
|
|
coinsUI.texture = cache:GetResource("Texture2D", "Urho2D/GoldIcon.png")
|
|
coinsUI:SetSize(50, 50)
|
|
coinsUI.imageRect = IntRect(0, 64, 60, 128)
|
|
coinsUI:SetAlignment(HA_LEFT, VA_TOP)
|
|
coinsUI:SetPosition(5, 5)
|
|
local coinsText = coinsUI:CreateChild("Text", "CoinsText")
|
|
coinsText:SetAlignment(HA_CENTER, VA_CENTER)
|
|
coinsText:SetFont(font, 24)
|
|
coinsText.textEffect = TE_SHADOW
|
|
coinsText.text = character2DNode:GetScriptObject().remainingCoins
|
|
|
|
-- Create the UI for displaying the remaining lifes
|
|
local lifeUI = ui.root:CreateChild("BorderImage", "Life")
|
|
lifeUI.texture = cache:GetResource("Texture2D", "Urho2D/imp/imp_all.png")
|
|
lifeUI:SetSize(70, 80)
|
|
lifeUI:SetAlignment(HA_RIGHT, VA_TOP)
|
|
lifeUI:SetPosition(-5, 5)
|
|
local lifeText = lifeUI:CreateChild("Text", "LifeText")
|
|
lifeText:SetAlignment(HA_CENTER, VA_CENTER)
|
|
lifeText:SetFont(font, 24)
|
|
lifeText.textEffect = TE_SHADOW
|
|
lifeText.text = LIFES
|
|
|
|
-- Create the fullscreen UI for start/end
|
|
local fullUI = ui.root:CreateChild("Window", "FullUI")
|
|
fullUI:SetStyleAuto()
|
|
fullUI:SetSize(ui.root.width, ui.root.height)
|
|
fullUI.enabled = false -- Do not react to input, only the 'Exit' and 'Play' buttons will
|
|
|
|
-- Create the title
|
|
local title = fullUI:CreateChild("BorderImage", "Title")
|
|
title:SetMinSize(fullUI.width, 50)
|
|
title.texture = cache:GetResource("Texture2D", "Textures/HeightMap.png")
|
|
title:SetFullImageRect()
|
|
title:SetAlignment(HA_CENTER, VA_TOP)
|
|
local titleText = title:CreateChild("Text", "TitleText")
|
|
titleText:SetAlignment(HA_CENTER, VA_CENTER)
|
|
titleText:SetFont(font, 24)
|
|
titleText.text = demoTitle
|
|
|
|
-- Create the image
|
|
local spriteUI = fullUI:CreateChild("BorderImage", "Sprite")
|
|
spriteUI.texture = cache:GetResource("Texture2D", "Urho2D/imp/imp_all.png")
|
|
spriteUI:SetSize(238, 271)
|
|
spriteUI:SetAlignment(HA_CENTER, VA_CENTER)
|
|
spriteUI:SetPosition(0, - ui.root.height / 4)
|
|
|
|
-- Create the 'EXIT' button
|
|
local exitButton = ui.root:CreateChild("Button", "ExitButton")
|
|
exitButton:SetStyleAuto()
|
|
exitButton.focusMode = FM_RESETFOCUS
|
|
exitButton:SetSize(100, 50)
|
|
exitButton:SetAlignment(HA_CENTER, VA_CENTER)
|
|
exitButton:SetPosition(-100, 0)
|
|
local exitText = exitButton:CreateChild("Text", "ExitText")
|
|
exitText:SetAlignment(HA_CENTER, VA_CENTER)
|
|
exitText:SetFont(font, 24)
|
|
exitText.text = "EXIT"
|
|
SubscribeToEvent(exitButton, "Released", "HandleExitButton")
|
|
|
|
-- Create the 'PLAY' button
|
|
local playButton = ui.root:CreateChild("Button", "PlayButton")
|
|
playButton:SetStyleAuto()
|
|
playButton.focusMode = FM_RESETFOCUS
|
|
playButton:SetSize(100, 50)
|
|
playButton:SetAlignment(HA_CENTER, VA_CENTER)
|
|
playButton:SetPosition(100, 0)
|
|
local playText = playButton:CreateChild("Text", "PlayText")
|
|
playText:SetAlignment(HA_CENTER, VA_CENTER)
|
|
playText:SetFont(font, 24)
|
|
playText.text = "PLAY"
|
|
SubscribeToEvent(playButton, "Released", "HandlePlayButton")
|
|
|
|
-- Create the instructions
|
|
local instructionText = ui.root:CreateChild("Text", "Instructions")
|
|
instructionText:SetFont(font, 15)
|
|
instructionText.textAlignment = HA_CENTER -- Center rows in relation to each other
|
|
instructionText.text = "Use WASD keys or Arrows to move\nPageUp/PageDown/MouseWheel to zoom\nF5/F7 to save/reload scene\n'Z' to toggle debug geometry\nSpace to fight"
|
|
instructionText:SetAlignment(HA_CENTER, VA_CENTER)
|
|
instructionText:SetPosition(0, ui.root.height / 4)
|
|
|
|
-- Show mouse cursor
|
|
input.mouseVisible = true
|
|
end
|
|
|
|
function HandleExitButton()
|
|
engine:Exit()
|
|
end
|
|
|
|
function HandlePlayButton()
|
|
-- Remove fullscreen UI and unfreeze the scene
|
|
if ui.root:GetChild("FullUI", true) then
|
|
ui.root:GetChild("FullUI", true):Remove()
|
|
scene_.updateEnabled = true
|
|
else
|
|
-- Reload the scene
|
|
ReloadScene(true)
|
|
end
|
|
|
|
-- Hide Instructions and Play/Exit buttons
|
|
ui.root:GetChild("Instructions", true).text = ""
|
|
ui.root:GetChild("ExitButton", true).visible = false
|
|
ui.root:GetChild("PlayButton", true).visible = false
|
|
|
|
-- Hide mouse cursor
|
|
input.mouseVisible = false
|
|
end
|
|
|
|
function SaveScene(initial)
|
|
local filename = demoFilename
|
|
if not initial then
|
|
filename = demoFilename .. "InGame"
|
|
end
|
|
|
|
scene_:SaveXML(fileSystem:GetProgramDir() .. "Data/Scenes/" .. filename .. ".xml")
|
|
end
|
|
|
|
function ReloadScene(reInit)
|
|
local filename = demoFilename
|
|
if not reInit then
|
|
filename = demoFilename .. "InGame"
|
|
end
|
|
|
|
scene_:LoadXML(fileSystem:GetProgramDir().."Data/Scenes/" .. filename .. ".xml")
|
|
-- After loading we have to reacquire the character scene node, as it has been recreated
|
|
-- Simply find by name as there's only one of them
|
|
character2DNode = scene_:GetChild("Imp", true)
|
|
if character2DNode == nil then
|
|
return
|
|
end
|
|
|
|
-- Set what value to use depending whether reload is requested from 'PLAY' button (reInit=true) or 'F7' key (reInit=false)
|
|
local character = character2DNode:GetScriptObject()
|
|
local lifes = character.remainingLifes
|
|
local coins =character.remainingCoins
|
|
if reInit then
|
|
lifes = LIFES
|
|
coins = character.maxCoins
|
|
end
|
|
|
|
-- Update lifes UI and value
|
|
local lifeText = ui.root:GetChild("LifeText", true)
|
|
lifeText.text = lifes
|
|
character.remainingLifes = lifes
|
|
|
|
-- Update coins UI and value
|
|
local coinsText = ui.root:GetChild("CoinsText", true)
|
|
coinsText.text = coins
|
|
character.remainingCoins = coins
|
|
end
|
|
|
|
function SpawnEffect(node)
|
|
local particleNode = node:CreateChild("Emitter")
|
|
particleNode:SetScale(0.5 / node.scale.x)
|
|
local particleEmitter = particleNode:CreateComponent("ParticleEmitter2D")
|
|
particleEmitter.effect = cache:GetResource("ParticleEffect2D", "Urho2D/sun.pex")
|
|
end
|
|
|
|
function PlaySound(soundName)
|
|
local soundNode = scene_:CreateChild("Sound")
|
|
local source = soundNode:CreateComponent("SoundSource")
|
|
source:Play(cache:GetResource("Sound", "Sounds/" .. soundName))
|
|
end
|
|
|
|
function CreateBackgroundSprite(info, scale, texture, animate)
|
|
local node = scene_:CreateChild("Background")
|
|
node.position = Vector3(info.mapWidth, info.mapHeight, 0) / 2
|
|
node:SetScale(scale)
|
|
local sprite = node:CreateComponent("StaticSprite2D")
|
|
sprite.sprite = cache:GetResource("Sprite2D", texture)
|
|
SetRandomSeed(time:GetSystemTime()) -- Randomize from system clock
|
|
sprite.color = Color(Random(0, 1), Random(0, 1), Random(0, 1), 1)
|
|
sprite.layer = -99
|
|
|
|
-- Create rotation animation
|
|
if animate then
|
|
local animation = ValueAnimation:new()
|
|
animation:SetKeyFrame(0, Variant(Quaternion(0, 0, 0)))
|
|
animation:SetKeyFrame(1, Variant(Quaternion(0, 0, 180)))
|
|
animation:SetKeyFrame(2, Variant(Quaternion(0, 0, 0)))
|
|
node:SetAttributeAnimation("Rotation", animation, WM_LOOP, 0.05)
|
|
end
|
|
end
|
|
|
|
|
|
-- Create XML patch instructions for screen joystick layout specific to this sample app
|
|
function GetScreenJoystickPatchString()
|
|
return
|
|
"<patch>" ..
|
|
" <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/attribute[@name='Is Visible']\" />" ..
|
|
" <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">Fight</replace>" ..
|
|
" <add sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]\">" ..
|
|
" <element type=\"Text\">" ..
|
|
" <attribute name=\"Name\" value=\"KeyBinding\" />" ..
|
|
" <attribute name=\"Text\" value=\"SPACE\" />" ..
|
|
" </element>" ..
|
|
" </add>" ..
|
|
" <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/attribute[@name='Is Visible']\" />" ..
|
|
" <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">Jump</replace>" ..
|
|
" <add sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]\">" ..
|
|
" <element type=\"Text\">" ..
|
|
" <attribute name=\"Name\" value=\"KeyBinding\" />" ..
|
|
" <attribute name=\"Text\" value=\"UP\" />" ..
|
|
" </element>" ..
|
|
" </add>" ..
|
|
"</patch>"
|
|
end
|