add slicer and load level function

This commit is contained in:
ohsqueezy 2023-06-22 14:40:06 -04:00
parent a71455fbd5
commit 237482eddb
11 changed files with 261 additions and 266 deletions

View File

@ -72,6 +72,10 @@ $(SB_SRC_DIR)VBO.o : $(addprefix $(SB_SRC_DIR),Log.hpp GLObject.hpp Attributes.h
$(SB_SRC_DIR)Attributes.o : $(addprefix $(SB_SRC_DIR),Log.hpp extension.hpp)
$(SB_SRC_DIR)Model.o : $(addprefix $(SB_SRC_DIR),extension.hpp Attributes.hpp Texture.hpp Carousel.hpp)
$(SB_SRC_DIR)Text.o : $(addprefix $(SB_SRC_DIR),Model.hpp Color.hpp)
$(SRC_DIR)Curve.o : $(addprefix $(SB_SRC_DIR),Attributes.hpp math.hpp extension.hpp)
$(SRC_DIR)Character.o : $(addprefix $(SB_SRC_DIR),Configuration.hpp Switch.hpp Selection.hpp Segment.hpp) $(addprefix $(SRC_DIR),Sprite.hpp Curve.hpp)
$(SRC_DIR)Pad.o : $(addprefix $(SRC_DIR),Model.hpp Switch.hpp)
$(SRC_DIR)Sprite.o : $(addprefix $(SRC_DIR),Model.hpp)
$(SRC_DIR)Cakefoot.o : $(SRC_H_FILES) $(SB_H_FILES)
%.o : %.cpp %.hpp
$(CXX) $(CXXFLAGS) $< -c -o $@

2
lib/sb

@ -1 +1 @@
Subproject commit 772c5482ddb0443771675056461fb32425f81ac1
Subproject commit c0852ba04f083bd89a8efa130f0832314a86a062

View File

@ -12,8 +12,10 @@ Cakefoot::Cakefoot()
SDL_SetHint(SDL_HINT_ORIENTATIONS, "Landscape");
#endif
/* Merge the level JSON */
_configuration.merge("resource/levels.json");
/* Attach a test print statement to a UI button that isn't fully implemented yet */
start_button.on_state_change([&](bool state, int count){
if (state)
{
@ -24,7 +26,7 @@ Cakefoot::Cakefoot()
}
});
/* subscribe to command events */
/* Subscribe to command events */
delegate().subscribe(&Cakefoot::respond, this);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEMOTION);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEBUTTONDOWN);
@ -54,10 +56,14 @@ Cakefoot::Cakefoot()
/* For each group of four control points, create a bezier, and add each of its vertices to a vector containing all the non-wrapped vertices, which
* is the full curve for the current level before being wrapped. */
std::vector<glm::vec2> unwrapped, segment;
std::vector<glm::vec3> unwrapped;
for (std::size_t jj = 0; jj < control.size() - 2; jj += 3)
{
segment = sb::bezier({control[jj], control[jj + 1], control[jj + 2], control[jj + 3]});
std::vector<glm::vec3> segment;
for (const glm::vec2& vertex : sb::bezier({control[jj], control[jj + 1], control[jj + 2], control[jj + 3]}))
{
segment.push_back({vertex, 0.0f});
}
unwrapped.insert(unwrapped.end(), segment.begin(), segment.end());
}
@ -162,6 +168,24 @@ Curve& Cakefoot::curve()
return curves[curve_index % curves.size()];
}
void Cakefoot::load_level(int index)
{
level_index = index;
curve_index = index;
character.reset(curve());
if (index == 1)
{
enemies = {
Slicer(curve(), 0.18f, 1.13168724f, 0.58641975f),
Slicer(curve(), 0.12f, 0.30864198f, 0.26748971f),
};
}
else
{
enemies = {};
}
}
void Cakefoot::respond(SDL_Event& event)
{
bool left_mouse_pressed = SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK;
@ -245,22 +269,19 @@ void Cakefoot::respond(SDL_Event& event)
else if (sb::Delegate::compare(event, "skip forward"))
{
curve_index++;
character.reset(curve());
load_level(level_index + 1);
}
else if (sb::Delegate::compare(event, "skip backward"))
{
curve_index--;
character.reset(curve());
load_level(level_index - 1);
}
else if (sb::Delegate::compare(event, "reset"))
{
field_of_view_y = 2 * glm::atan(1.0 / camera_position.z);
rotation = {0.0f, 0.0f};
curve_index = 0;
character.reset(curve());
load_level(0);
}
/* Taken from mallinfo man page, log a profile of the memory when the command is sent. */
@ -332,8 +353,12 @@ void Cakefoot::update(float timestamp)
set_framerate(configuration()["display"]["framerate"]);
glUniform1f(uniform["time"], timer.elapsed());
/* Update character, along the curve, using the timer to determine movement since last frame. */
/* Update character, along the curve, using the timer to determine movement since last frame, and update enemies. */
character.update(curve(), timer);
for (Slicer& slicer : enemies)
{
slicer.update(timer);
}
/* Transformation for rotating the model space and looking at the center of the field of play from the camera position. */
view = glm::lookAt(camera_position, {0.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}) *
@ -376,8 +401,15 @@ void Cakefoot::update(float timestamp)
}
curve().color.disable();
/* Draw cake */
/* Draw enemies */
sb::Plane::position->bind("vertex_position", shader_program);
for (Slicer& slicer : enemies)
{
slicer.sprite.bind_texture(uniform["texture enabled"]);
slicer.sprite.draw(uniform["mvp"], view, projection);
}
/* Draw cake */
character.sprite.translate(glm::vec3{cake_translation, 0.0f});
character.sprite.bind_texture(uniform["texture enabled"]);
character.sprite.draw(uniform["mvp"], view, projection);

View File

@ -42,6 +42,7 @@
#include "Configuration.hpp"
#include "Timer.hpp"
#include "Text.hpp"
#include "Segment.hpp"
/* Project classes */
#include "Character.hpp"
@ -49,8 +50,101 @@
#include "Pad.hpp"
#include "Curve.hpp"
/*!
* Create an enemy which moves back and forth, cutting the curve perpendicular to the angle of the curve at a given point.
*/
class Slicer
{
private:
std::reference_wrapper<const Curve> curve;
float relative = 0.0f, speed = 0.0f, stray = 0.0f;
bool toward_end = true;
/*!
*/
glm::vec3 center() const
{
return curve.get().relative(relative);
}
/*!
*/
float angle() const
{
int index = curve.get().index(relative);
int next_index = std::min(index + 1, curve.get().length() - 1);
int prev_index = std::max(index - 1, 0);
return sb::angle_between(curve.get()[prev_index], curve.get()[next_index]) + glm::half_pi<float>();
}
/*!
*/
glm::vec3 start() const
{
return {sb::endpoint(center(), angle(), stray), 0.0f};
}
/*!
*/
glm::vec3 end() const
{
return {sb::endpoint(center(), angle() + glm::pi<float>(), stray), 0.0f};
}
public:
glm::vec3 position = {0.0f, 0.0f, 0.0f};
Sprite sprite = Sprite("resource/slicer/slicer-1.png", glm::vec2{12.0f / 486.0f});
Slicer(const Curve& curve, float relative, float speed = 0.51440329f, float stray = 0.24691358f) :
curve(std::reference_wrapper<const Curve>(curve)), relative(relative), speed(speed), stray(stray)
{
reset();
};
void reset()
{
position = start();
sprite.translate(curve.get().wrap(position));
}
void update(sb::Timer timer)
{
glm::vec3 destination;
if (toward_end)
{
destination = end();
}
else
{
destination = start();
}
float distance = glm::distance(position, destination);
float move = timer.delta(speed);
if (distance < move)
{
position = destination;
move -= distance;
toward_end = !toward_end;
}
position += glm::vec3{sb::Segment(position, destination).step(move), 0.0f};
sprite.translate(curve.get().wrap(position));
}
};
/*!
* The main game object. There is currently only support for one of these to exist at a time.
*
* Note: the conversion for distance from the original 864x468 coordinate system to this program's NDC coordinate system is
*
* f(distance) = distance / 486.0 * 2
*
* Note: the conversion for speed values from the original Python version's per-frame, fixed 25fps, 864x468 coordinate system to this program's
* per-second, adjustable framerate, NDC coordinate system is below. This gives the speed in amount of NDC to travel per second.
*
* f(speed) = speed / 486.0 * 25 * 2
*/
class Cakefoot : public sb::Game
{
@ -82,7 +176,7 @@ private:
/* Member vars */
std::shared_ptr<SDL_Cursor> poke, grab;
int effect_id = EFFECT_NONE, previous_frames_per_second = 0, curve_index = 0, curve_byte_count = 0;
int effect_id = EFFECT_NONE, previous_frames_per_second = 0, curve_index = 0, curve_byte_count = 0, level_index = 0;
std::map<std::string, GLuint> uniform;
GLuint shader_program;
glm::mat4 view {1.0f}, projection {1.0f}, mvp;
@ -97,7 +191,7 @@ private:
glm::vec2 rotation = {0.0f, 0.0f};
sb::Text fps{font()};
std::vector<Curve> curves;
std::vector<Sprite> enemies;
std::vector<Slicer> enemies;
/*!
* Create GL context via super class and load vertices, UV data, and shaders.
@ -109,6 +203,13 @@ private:
*/
Curve& curve();
/*!
* Change the level to the given index. Load enemies, update the curve index.
*
* @param index index of the level to load
*/
void load_level(int index);
public:
/*!

View File

@ -1,16 +1,55 @@
#include "Curve.hpp"
void Curve::add(const std::vector<glm::vec3>& vertices)
{
sb::extend(unwrapped, vertices);
for (const std::vector<glm::vec3>& wrapped : sb::wrap_curve(vertices, {-aspect, -1.0f, 0.0f}, {aspect, 1.0f, 1.0f}))
{
position.push_back(sb::Attributes(wrapped));
for (std::size_t jj = 0; jj < wrapped.size(); jj++)
{
color.add(1.0f, 1.0f, 1.0f, 1.0f);
}
}
}
int Curve::length() const
{
return unwrapped.size();
}
const glm::vec2& Curve::front() const
const glm::vec3& Curve::front() const
{
return unwrapped.front();
}
const glm::vec2& Curve::operator[](int index) const
std::size_t Curve::size() const
{
std::size_t byte_count = 0;
for (const auto& attr : position)
{
byte_count += attr.size();
}
byte_count += color.size();
return byte_count;
}
glm::vec3 Curve::operator[](int index) const
{
return unwrapped[index];
}
int Curve::index(float relative) const
{
return std::round(glm::mod(relative, 1.0f) * length());
}
glm::vec3 Curve::relative(float relative) const
{
return unwrapped[index(relative)];
}
glm::vec3 Curve::wrap(const glm::vec3& vertex) const
{
return sb::wrap_point(vertex, {-aspect, -1.0f, 0.0f}, {aspect, 1.0f, 1.0f});
}

View File

@ -4,6 +4,7 @@
#include "glm/glm.hpp"
#include "Attributes.hpp"
#include "math.hpp"
#include "extension.hpp"
class Curve
{
@ -11,24 +12,20 @@ class Curve
public:
float aspect;
std::vector<glm::vec2> unwrapped;
std::vector<glm::vec3> unwrapped;
std::vector<sb::Attributes> position;
sb::Attributes color;
Curve (float aspect) : aspect(aspect) {}
void add(const std::vector<glm::vec2>& vertices)
{
unwrapped = vertices;
for (const std::vector<glm::vec2>& wrapped : sb::wrap_curve(vertices, {-aspect, -1.0f}, {aspect, 1.0f}))
{
position.push_back(sb::Attributes(wrapped));
for (std::size_t jj = 0; jj < wrapped.size(); jj++)
{
color.add(1.0f, 1.0f, 1.0f, 1.0f);
}
}
}
/*!
* Add a vector of vertices to the curve. The vertices do not have to be wrapped to the screen space.
*
* The vertices will be stored both as the original vertices passed and as sb::Attributes wrapped to screen space if necessary.
*
* @vertices vector of vertices to add to curve
*/
void add(const std::vector<glm::vec3>& vertices);
/*!
* @return number of vertices in the unwrapped curve
@ -38,25 +35,46 @@ public:
/*!
* @return the first point of the unwrapped curve
*/
const glm::vec2& front() const;
const glm::vec3& front() const;
/*!
* @return size in bytes of the GL vertex data
*/
std::size_t size() const
{
std::size_t byte_count = 0;
for (const auto& attr : position)
{
byte_count += attr.size();
}
byte_count += color.size();
return byte_count;
}
std::size_t size() const;
/*!
* @param index index of the vertex in the unwrapped curve vertices list
* @return vertex in the unwrapped vertices list at index
*/
const glm::vec2& operator[](int index) const;
glm::vec3 operator[](int index) const;
/*!
* Get the index of the curve at the relative position, where 0 is the beginning of the curve, and 1 is the end. The position can
* be outside of the range 0 - 1, in which case it will be wrapped to between 0 and 1.
*
* The returned index can be used with Curve::operator[](int) to get the vertex at the relative position.
*
* @param relative position between 0 and 1 relative to the ends of the curve
* @return curve index at position
*/
int index(float relative) const;
/*!
* Get the vertex at the given relative position on the curve, where 0 is the beginning of the curve, and 1 is the end. The position
* can be outside of the range 0 - 1, in which case it will be wrapped to between 0 and 1.
*
* This is a shortcut to using Curve::index(float) with Curve::operator[](int)
*
* @param relative position between 0 and 1 relative to the ends of the curve
* @return vertex value at position
*/
glm::vec3 relative(float relative) const;
/*!
* Wrap a vertex into this curve's space. The vertex doesn't need to be along the curve.
*
* @param vertex vertex in world space to wrap into the curve's world space
* @return wrapped vertex
*/
glm::vec3 wrap(const glm::vec3& vertex) const;
};

View File

@ -10,23 +10,23 @@
*
* Each instance:
*
* - Shares vertices and UV in VBO
* - Has its own Texture representing the button on-screen
* - Has its own response to click
* - Shares mouse collision code
* - Has its own translate + scale transformation
* - Shares vertices and UV in VBO
* - Has its own Texture representing the button on-screen
* - Has its own response to click
* - Shares mouse collision code
* - Has its own translate + scale transformation
*
* Example:
*
* glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, {0, 0, 1}}) *
* glm::vec3({-1, -1, 1});
* std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) *
* glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl;
* Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function<void()>()};
* const std::vector<glm::vec2>& p_position = *p.attributes("position");
* glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1};
* std::cout << p.transformation() << std::endl << final_position << std::endl;
* assert(final_position == glm::vec4({w.x, w.y, 0, 1}));
* glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, {0, 0, 1}}) *
* glm::vec3({-1, -1, 1});
* std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) *
* glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl;
* Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function<void()>()};
* const std::vector<glm::vec2>& p_position = *p.attributes("position");
* glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1};
* std::cout << p.transformation() << std::endl << final_position << std::endl;
* assert(final_position == glm::vec4({w.x, w.y, 0, 1}));
*/
template<typename return_type = void, typename... arguments>
class Pad : public sb::Plane
@ -141,6 +141,18 @@ public:
/*!
* Set the function that will run when a pad object is clicked.
*
* Example, always keep state true and print "Hello, World!" whenever the pad is clicked,
*
* start_button.on_state_change([&](bool state, int count){
* if (state)
* {
* std::ostringstream message;
* message << "Hello, " << state << " World! " << count;
* sb::Log::log(message);
* start_button.press(1);
* }
* });
*
* @param on_state_change reaction function which accepts a boolean as its first argument
*/
void on_state_change(Reaction reaction)

View File

@ -1,44 +0,0 @@
#version 300 es
/* _______________ ,--------------------------------------------------------.
//`````````````\\ \ \
//~~~~~~~~~~~~~~~\\ \ by @ohsqueezy & @sleepin \
//=================\\ \ [ohsqueezy.itch.io] [sleepin.itch.io] \
// \\ \ \
// \\ \ zlib licensed code at [git.nugget.fun/nugget/gunkiss] \
// ☆ GUNKISS ☆ \\ \ \
//_________________________\\ `--------------------------------------------------------*/
/* The precision declaration is required by OpenGL ES */
precision mediump float;
in vec2 uv;
uniform sampler2D base_texture;
uniform vec3 blend_min_hsv;
uniform float time;
uniform bool scroll;
out vec4 outputColor;
/* from http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl, licensed under WTFPL */
vec3 hsv2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main(void)
{
if (scroll)
{
ivec2 texture_size = textureSize(base_texture, 0);
float speed = time * 35.0;
outputColor = texelFetch(base_texture, ivec2(mod(vec2(gl_FragCoord.x + speed, gl_FragCoord.y - speed), vec2(texture_size))), 0);
}
else
{
outputColor = texture(base_texture, uv);
}
/* apply blending, leaving alpha unchanged */
outputColor.xyz = min(outputColor.xyz, hsv2rgb(blend_min_hsv));
}

View File

@ -1,24 +0,0 @@
#version 300 es
/* _______________ ,--------------------------------------------------------.
//`````````````\\ \ \
//~~~~~~~~~~~~~~~\\ \ by @ohsqueezy & @sleepin \
//=================\\ \ [ohsqueezy.itch.io] [sleepin.itch.io] \
// \\ \ \
// \\ \ zlib licensed code at [git.nugget.fun/nugget/gunkiss] \
// ☆ GUNKISS ☆ \\ \ \
//_________________________\\ `--------------------------------------------------------*/
/* The precision declaration is required by OpenGL ES */
precision mediump float;
in vec2 in_position;
in vec2 vertex_uv;
uniform mat4 transformation;
out vec2 uv;
void main(void)
{
gl_Position = transformation * vec4(in_position, 0, 1);
uv = vertex_uv;
}

View File

@ -1,76 +0,0 @@
#version 300 es
/* >> Cakefoot by https://foam.shampoo.ooo/ << */
/* The precision declaration is required by OpenGL ES */
precision mediump float;
#define TRANSFORMATION_NONE 0
#define TRANSFORMATION_SQUIRCLE 1
in vec2 fragment_uv;
in vec3 ex_color;
in float x_center_proximity;
in vec3 original_coordinates;
in vec3 clip_coordinates;
uniform sampler2D pudding_texture;
uniform int uv_transformation;
uniform float coordinate_bound;
out vec4 output_color;
/* [-coordinate_bound, coordinate_bound] arbitrary box coordinates to [-1, 1] normalized coordinates */
vec2 normalize_coordinates(vec2 coordinates)
{
return coordinates / coordinate_bound;
}
/* [-1, 1] box coordinates to [0, 1] UV coordinates */
vec2 coordinates_to_uv(vec2 coordinates)
{
return (1.0 + coordinates) / 2.0;
}
/* coordinates in circle with radius <= 1 to box coordinates in [-1, 1] */
vec2 circle_to_box(vec2 circle)
{
float u = circle.x;
float v = circle.y;
float u_sq = pow(u, 2.0);
float v_sq = pow(v, 2.0);
float rt_2 = sqrt(2.0);
float x = 0.5 * sqrt(2.0 + 2.0 * u * rt_2 + u_sq - v_sq) - 0.5 * sqrt(2.0 - 2.0 * u * rt_2 + u_sq - v_sq);
float y = 0.5 * sqrt(2.0 + 2.0 * v * rt_2 - u_sq + v_sq) - 0.5 * sqrt(2.0 - 2.0 * v * rt_2 - u_sq + v_sq);
return vec2(x, y);
}
/* Apply color passed in from the vertex shader, compressing to one of 16 colors. Add retro effect
* by alternately darkening and lightening 2x2 pixel areas in a checker pattern. Add shadowing by
* brightening the color based on how near it is to the center in the X-dimension */
void retro()
{
vec3 shadowed = min(ex_color, 1.0);
float dx = abs(floor(gl_FragCoord[0]) - 480.0) / 480.0;
if (int(floor(gl_FragCoord[0] / 2.0) + floor(gl_FragCoord[1]) / 2.0) % 2 == 0)
{
output_color = vec4(shadowed * 1.2, 1);
}
else
{
output_color = vec4(shadowed * 0.7, 1);
}
output_color[0] = float(int(output_color[0] * 4.0)) / 4.0;
output_color[1] = float(int(output_color[1] * 4.0)) / 4.0;
output_color[2] = float(int(output_color[2] * 4.0)) / 4.0;
output_color *= x_center_proximity;
}
void main()
{
vec2 uv = fragment_uv;
// if (uv_transformation == TRANSFORMATION_SQUIRCLE)
// {
// vec2 normalized_circle_coordinates = normalize_coordinates(vec2(original_coordinates.x, original_coordinates.z));
// uv = coordinates_to_uv(circle_to_box(normalized_circle_coordinates));
// }
output_color = texture(pudding_texture, uv);
}

View File

@ -1,67 +0,0 @@
#version 300 es
/* >> Cakefoot by https://foam.shampoo.ooo/ << */
/* The precision declaration is required by OpenGL ES */
precision mediump float;
#define PI 3.1415926535897932384626433832795
#define AMPLITUDE 0.2
#define PERIOD .5
#define WAVELENGTH 2.5
#define EFFECT_NONE 0
#define EFFECT_SNAKE 1
#define EFFECT_WOBBLE 2
in vec3 vertex_position;
in vec3 vertex_color;
in vec2 vertex_uv;
uniform mat4 mvp;
uniform float time;
uniform int effect;
out vec3 ex_color;
out float x_center_proximity;
out vec2 fragment_uv;
out vec3 original_coordinates;
out vec3 clip_coordinates;
/* Offset X-coordinate according to the time step and Y-coordinate to create a wobble effect when run on
* flattened coordinates (after projection matrix has been applied) */
void wobble()
{
gl_Position.x += sin(time * 10.0) * vertex_position.y / 2.0;
}
/* Contort the X-coordinate according the time step and Y-coordinate using the sine function. This contorts
* the model into a sine wave along the Y-axis. It also moves the sine wave along the Y-axis using the time
* step uniform. The shape can be edited by changing the defintions for amplitude (maximum distance from
* Y-axis), wavelength (length in GL model coordinates between peaks), and period (amount of time in
* seconds it takes to loop through one wavelength cycle). */
void snake()
{
gl_Position = vec4(
vertex_position.x + AMPLITUDE * sin(2.0 * PI / PERIOD * (time + vertex_position.y * PERIOD / WAVELENGTH)), vertex_position.yz, 1);
}
void main()
{
// if (effect == EFFECT_SNAKE)
// {
// snake();
// }
// else
// {
gl_Position = vec4(vertex_position, 1);
// }
gl_Position = mvp * gl_Position;
// if (effect == EFFECT_WOBBLE)
// {
// wobble();
// }
/* passing to fragment program */
ex_color = vertex_color;
x_center_proximity = 1.8 - abs(vertex_position[0]);
fragment_uv = vertex_uv;
original_coordinates = vertex_position;
clip_coordinates = gl_Position.xyz;
}