-module(mascarpone). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/mascarpone.gleam"). -export([main/0]). -export_type([step/0, template/0, model/0, msg/0]). -type step() :: welcome | lustre_choice | template_choice | complete | {failed, binary()}. -type template() :: two_d_game | three_d_game | physics_demo. -type model() :: {model, step(), binary(), boolean(), boolean(), gleam@option:option(template())}. -type msg() :: next_step | {set_lustre, boolean()} | {set_template, template()} | generate. -file("src/mascarpone.gleam", 75). -spec init(binary()) -> {model(), list(fun(() -> msg()))}. init(Project_name) -> {{model, welcome, Project_name, true, false, none}, []}. -file("src/mascarpone.gleam", 143). -spec view_welcome() -> shore@internal:node_(msg()). view_welcome() -> shore@ui:col( [shore@ui:text_styled( <<"╔═══════════════════════════════════╗"/utf8>>, {some, cyan}, none ), shore@ui:text_styled( <<"║ 🎮 Tiramisu Project Creator 🎮 ║"/utf8>>, {some, cyan}, none ), shore@ui:text_styled( <<"║ Gleam 3D Game Engine ║"/utf8>>, {some, cyan}, none ), shore@ui:text_styled( <<"╚═══════════════════════════════════╝"/utf8>>, {some, cyan}, none ), shore@ui:text(<<""/utf8>>), shore@ui:text(<<"Welcome to Tiramisu! 🍰"/utf8>>), shore@ui:text(<<""/utf8>>), shore@ui:text( <<"This wizard will help you set up Tiramisu for your game project."/utf8>> ), shore@ui:text(<<""/utf8>>), shore@ui:hr(), shore@ui:text(<<""/utf8>>), shore@ui:button(<<"Continue"/utf8>>, enter, next_step)] ). -file("src/mascarpone.gleam", 176). -spec view_lustre_choice() -> shore@internal:node_(msg()). view_lustre_choice() -> shore@ui:col( [shore@ui:text_styled(<<"Lustre Integration"/utf8>>, {some, cyan}, none), shore@ui:text(<<""/utf8>>), shore@ui:text( <<"Lustre allows you to create UI overlays for your game"/utf8>> ), shore@ui:text( <<"(menus, HUDs, dialogs, etc.) using a reactive framework."/utf8>> ), shore@ui:text(<<""/utf8>>), shore@ui:hr(), shore@ui:text(<<""/utf8>>), shore@ui:text(<<"Include Lustre?"/utf8>>), shore@ui:text(<<""/utf8>>), shore@ui:button( <<"[Y] Yes (recommended)"/utf8>>, {char, <<"y"/utf8>>}, {set_lustre, true} ), shore@ui:button( <<"[N] No"/utf8>>, {char, <<"n"/utf8>>}, {set_lustre, false} )] ). -file("src/mascarpone.gleam", 192). -spec view_template_choice() -> shore@internal:node_(msg()). view_template_choice() -> shore@ui:col( [shore@ui:text_styled(<<"Project Template"/utf8>>, {some, cyan}, none), shore@ui:text(<<""/utf8>>), shore@ui:button( <<"[1] 2D Game - Orthographic camera and sprite setup"/utf8>>, {char, <<"1"/utf8>>}, {set_template, two_d_game} ), shore@ui:button( <<"[2] 3D Game - Perspective camera with lighting"/utf8>>, {char, <<"2"/utf8>>}, {set_template, three_d_game} ), shore@ui:button( <<"[3] Physics Demo - Physics-enabled objects"/utf8>>, {char, <<"3"/utf8>>}, {set_template, physics_demo} )] ). -file("src/mascarpone.gleam", 252). -spec view_error(binary()) -> shore@internal:node_(msg()). view_error(Msg) -> shore@ui:col( [shore@ui:text_styled(<<"❌ Error occurred"/utf8>>, {some, red}, none), shore@ui:text(<<""/utf8>>), shore@ui:text(Msg)] ). -file("src/mascarpone.gleam", 260). -spec template_name(template()) -> binary(). template_name(Template) -> case Template of two_d_game -> <<"2D Game"/utf8>>; three_d_game -> <<"3D Game"/utf8>>; physics_demo -> <<"Physics Demo"/utf8>> end. -file("src/mascarpone.gleam", 214). -spec view_complete(model()) -> shore@internal:node_(msg()). view_complete(Model) -> shore@ui:col( [shore@ui:text_styled( <<"✅ Project setup complete!"/utf8>>, {some, green}, none ), shore@ui:text(<<""/utf8>>), shore@ui:text( <<"Project: "/utf8, (erlang:element(3, Model))/binary>> ), shore@ui:text( <<"Template: "/utf8, (template_name( gleam@option:unwrap( erlang:element(6, Model), three_d_game ) ))/binary>> ), shore@ui:text(<<"Lustre: "/utf8, (case erlang:element(4, Model) of true -> <<"Yes"/utf8>>; false -> <<"No"/utf8>> end)/binary>>), shore@ui:text(<<"Physics: "/utf8, (case erlang:element(5, Model) of true -> <<"Yes"/utf8>>; false -> <<"No"/utf8>> end)/binary>>), shore@ui:text(<<""/utf8>>), shore@ui:hr(), shore@ui:text(<<""/utf8>>), shore@ui:text(<<"Next steps:"/utf8>>), shore@ui:text(<<""/utf8>>), shore@ui:text(<<"1. Start the dev server:"/utf8>>), shore@ui:text_styled( <<" gleam run -m lustre/dev start"/utf8>>, {some, cyan}, none ), shore@ui:text(<<""/utf8>>), shore@ui:text( <<"2. Open http://localhost:1234 in your browser"/utf8>> ), shore@ui:text(<<""/utf8>>), shore@ui:text(<<"Happy game development! 🎮"/utf8>>), shore@ui:text(<<""/utf8>>), shore@ui:text(<<"Press Ctrl + X to leave"/utf8>>)] ). -file("src/mascarpone.gleam", 131). -spec view(model()) -> shore@internal:node_(msg()). view(Model) -> case erlang:element(2, Model) of welcome -> view_welcome(); lustre_choice -> view_lustre_choice(); template_choice -> view_template_choice(); complete -> view_complete(Model); {failed, Msg} -> view_error(Msg) end. -file("src/mascarpone.gleam", 310). -spec find_root(binary()) -> binary(). find_root(Path) -> Toml = filepath:join(Path, <<"gleam.toml"/utf8>>), case simplifile_erl:is_file(Toml) of {ok, false} -> find_root(filepath:join(Path, <<".."/utf8>>)); {error, _} -> find_root(filepath:join(Path, <<".."/utf8>>)); {ok, true} -> Path end. -file("src/mascarpone.gleam", 288). -spec get_project_name() -> {ok, binary()} | {error, snag:snag()}. get_project_name() -> Root = find_root(<<"."/utf8>>), Toml_path = filepath:join(Root, <<"gleam.toml"/utf8>>), gleam@result:'try'( begin _pipe = simplifile:read(Toml_path), snag:map_error( _pipe, fun(_) -> <<"Could not read gleam.toml"/utf8>> end ) end, fun(Content) -> gleam@result:'try'( begin _pipe@1 = tom:parse(Content), snag:map_error( _pipe@1, fun(_) -> <<"Could not parse gleam.toml"/utf8>> end ) end, fun(Toml) -> gleam@result:'try'( begin _pipe@2 = tom:get_string(Toml, [<<"name"/utf8>>]), snag:map_error( _pipe@2, fun(_) -> <<"Could not find project name in gleam.toml"/utf8>> end ) end, fun(Name) -> {ok, Name} end ) end ) end ). -file("src/mascarpone.gleam", 319). -spec update_gleam_toml(binary(), boolean()) -> {ok, nil} | {error, snag:snag()}. update_gleam_toml(Project_name, Include_lustre) -> Root = find_root(<<"."/utf8>>), Toml_path = filepath:join(Root, <<"gleam.toml"/utf8>>), gleam@result:'try'( begin _pipe = simplifile:read(Toml_path), snag:map_error( _pipe, fun(_) -> <<"Could not read gleam.toml"/utf8>> end ) end, fun(Content) -> Content@1 = case gleam_stdlib:contains_string( Content, <<"target ="/utf8>> ) of true -> Content; false -> gleam@string:replace( Content, <<<<"name = \""/utf8, Project_name/binary>>/binary, "\""/utf8>>, <<<<"name = \""/utf8, Project_name/binary>>/binary, "\"\ntarget = \"javascript\""/utf8>> ) end, Deps = [<<"tiramisu = \">= 2.0.0 and < 3.0.0\""/utf8>>, <<"vec = \">= 3.0.1 and < 4.0.0\""/utf8>>], Dev_deps = [<<"lustre_dev_tools = \">= 2.0.2 and <= 3.0.0\""/utf8>>], Deps@1 = case Include_lustre of true -> lists:append( Deps, [<<"lustre = \">= 5.0.0 and < 6.0.0\""/utf8>>] ); false -> Deps end, Content_with_deps = case gleam_stdlib:contains_string( Content@1, <<"[dependencies]"/utf8>> ) of true -> gleam@list:fold( Deps@1, Content@1, fun(Acc, Dep) -> gleam@string:replace( Acc, <<"[dependencies]"/utf8>>, <<"[dependencies]\n"/utf8, Dep/binary>> ) end ); false -> <<<>/binary, (gleam@string:join(Deps@1, <<"\n"/utf8>>))/binary>> end, Content_with_dev_deps = case gleam_stdlib:contains_string( Content_with_deps, <<"[dev-dependencies]"/utf8>> ) of true -> gleam@list:fold( Dev_deps, Content_with_deps, fun(Acc@1, Dep@1) -> gleam@string:replace( Acc@1, <<"[dev-dependencies]"/utf8>>, <<"[dev-dependencies]\n"/utf8, Dep@1/binary>> ) end ); false -> <<<>/binary, (gleam@string:join(Dev_deps, <<"\n"/utf8>>))/binary>> end, Lustre_config = <<"\n\n[tools.lustre.html] scripts = [ { type = \"importmap\", content = \"{ \\\"imports\\\": { \\\"three\\\": \\\"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js\\\", \\\"three/addons/\\\": \\\"https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/\\\", \\\"@dimforge/rapier3d-compat\\\": \\\"https://cdn.jsdelivr.net/npm/@dimforge/rapier3d-compat@0.11.2/+esm\\\" } }\" } ] stylesheets = [ { content = \"body { margin: 0; padding: 0; overflow: hidden; }\" } ] "/utf8>>, Final_content = case gleam_stdlib:contains_string( Content_with_dev_deps, <<"[tools.lustre.html]"/utf8>> ) of true -> Content_with_dev_deps; false -> <> end, _pipe@1 = simplifile:write(Toml_path, Final_content), snag:map_error( _pipe@1, fun(_) -> <<"Could not write gleam.toml"/utf8>> end ) end ). -file("src/mascarpone.gleam", 406). -spec create_gitignore() -> {ok, nil} | {error, snag:snag()}. create_gitignore() -> Root = find_root(<<"."/utf8>>), Gitignore_path = filepath:join(Root, <<".gitignore"/utf8>>), Content = <<"*.beam *.ez /build erl_crash.dump /priv .DS_Store node_modules/ dist/ .lustre/"/utf8>>, _pipe = simplifile:write(Gitignore_path, Content), snag:map_error(_pipe, fun(_) -> <<"Could not write .gitignore"/utf8>> end). -file("src/mascarpone.gleam", 442). -spec generate_2d_template() -> binary(). generate_2d_template() -> <<"/// 2D Game Example - Orthographic Camera import gleam/float import gleam/option import tiramisu import tiramisu/background import tiramisu/camera import tiramisu/effect.{type Effect} import tiramisu/geometry import tiramisu/light import tiramisu/material import tiramisu/scene import tiramisu/transform import vec/vec3 pub type Model { Model(time: Float) } pub type Msg { Tick } pub fn main() -> Nil { tiramisu.run( dimensions: option.None, background: background.Color(0x1a1a2e), init: init, update: update, view: view, ) } fn init(_ctx: tiramisu.Context(String)) -> #(Model, Effect(Msg), option.Option(_)) { #(Model(time: 0.0), effect.tick(Tick), option.None) } fn update( model: Model, msg: Msg, ctx: tiramisu.Context(String), ) -> #(Model, Effect(Msg), option.Option(_)) { case msg { Tick -> { let new_time = model.time +. ctx.delta_time #(Model(time: new_time), effect.tick(Tick), option.None) } } } fn view(model: Model, ctx: tiramisu.Context(String)) -> List(scene.Node(String)) { let cam = camera.camera_2d( width: float.round(ctx.canvas_width), height: float.round(ctx.canvas_height), ) let assert Ok(sprite_geom) = geometry.plane(width: 50.0, height: 50.0) let assert Ok(sprite_mat) = material.basic(color: 0xff0066, transparent: False, opacity: 1.0, map: option.None) [ scene.Camera( id: \"camera\", camera: cam, transform: transform.at(position: vec3.Vec3(0.0, 0.0, 20.0)), look_at: option.None, active: True, viewport: option.None, ), scene.Light( id: \"ambient\", light: { let assert Ok(light) = light.ambient(color: 0xffffff, intensity: 1.0) light }, transform: transform.identity, ), scene.Mesh( id: \"sprite\", geometry: sprite_geom, material: sprite_mat, transform: transform.Transform( position: vec3.Vec3(0.0, 0.0, 0.0), rotation: vec3.Vec3(0.0, 0.0, model.time), scale: vec3.Vec3(1.0, 1.0, 1.0), ), physics: option.None, ), ] } "/utf8>>. -file("src/mascarpone.gleam", 533). -spec generate_3d_template() -> binary(). generate_3d_template() -> <<"/// 3D Game Example - Perspective Camera with Lighting import gleam/option import tiramisu import tiramisu/background import tiramisu/camera import tiramisu/effect.{type Effect} import tiramisu/geometry import tiramisu/light import tiramisu/material import tiramisu/scene import tiramisu/transform import vec/vec3 pub type Model { Model(time: Float) } pub type Msg { Tick } pub fn main() -> Nil { tiramisu.run( dimensions: option.None, background: background.Color(0x1a1a2e), init: init, update: update, view: view, ) } fn init(_ctx: tiramisu.Context(String)) -> #(Model, Effect(Msg), option.Option(_)) { #(Model(time: 0.0), effect.tick(Tick), option.None) } fn update( model: Model, msg: Msg, ctx: tiramisu.Context(String), ) -> #(Model, Effect(Msg), option.Option(_)) { case msg { Tick -> { let new_time = model.time +. ctx.delta_time #(Model(time: new_time), effect.tick(Tick), option.None) } } } fn view(model: Model, _ctx: tiramisu.Context(String)) -> List(scene.Node(String)) { let assert Ok(cam) = camera.perspective(field_of_view: 75.0, near: 0.1, far: 1000.0) let assert Ok(sphere_geom) = geometry.sphere(radius: 1.0, width_segments: 32, height_segments: 32) let assert Ok(sphere_mat) = material.new() |> material.with_color(0x0066ff) |> material.build let assert Ok(ground_geom) = geometry.plane(width: 20.0, height: 20.0) let assert Ok(ground_mat) = material.new() |> material.with_color(0x808080) |> material.build [ scene.Camera( id: \"camera\", camera: cam, transform: transform.at(position: vec3.Vec3(0.0, 5.0, 10.0)), look_at: option.Some(vec3.Vec3(0.0, 0.0, 0.0)), active: True, viewport: option.None, ), scene.Light( id: \"ambient\", light: { let assert Ok(light) = light.ambient(color: 0xffffff, intensity: 0.5) light }, transform: transform.identity, ), scene.Light( id: \"directional\", light: { let assert Ok(light) = light.directional(color: 0xffffff, intensity: 0.8) light }, transform: transform.at(position: vec3.Vec3(10.0, 10.0, 10.0)), ), scene.Mesh( id: \"sphere\", geometry: sphere_geom, material: sphere_mat, transform: transform.at(position: vec3.Vec3(0.0, 0.0, 0.0)), physics: option.None, ), scene.Mesh( id: \"ground\", geometry: ground_geom, material: ground_mat, transform: transform.Transform( position: vec3.Vec3(0.0, -2.0, 0.0), rotation: vec3.Vec3(-1.57, 0.0, 0.0), scale: vec3.Vec3(1.0, 1.0, 1.0), ), physics: option.None, ), ] } "/utf8>>. -file("src/mascarpone.gleam", 637). -spec generate_physics_template() -> binary(). generate_physics_template() -> <<"/// Physics Demo - Falling Cubes /// Demonstrates physics simulation with Rapier3D import gleam/option import tiramisu import tiramisu/background import tiramisu/camera import tiramisu/effect.{type Effect} import tiramisu/geometry import tiramisu/light import tiramisu/material import tiramisu/physics import tiramisu/scene import tiramisu/transform import vec/vec3 pub type Id { Camera Ambient Directional Ground Cube1 Cube2 } pub type Model { Model } pub type Msg { Tick } pub fn main() -> Nil { tiramisu.run( dimensions: option.None, background: background.Color(0x1a1a2e), init: init, update: update, view: view, ) } fn init(_ctx: tiramisu.Context(Id)) -> #(Model, Effect(Msg), option.Option(_)) { // Initialize physics world with gravity let physics_world = physics.new_world( physics.WorldConfig(gravity: vec3.Vec3(0.0, -9.81, 0.0), correspondances: [ #(Cube1, \"cube-1\"), #(Cube2, \"cube-2\"), #(Ground, \"ground\"), ]), ) #(Model, effect.tick(Tick), option.Some(physics_world)) } fn update( model: Model, msg: Msg, ctx: tiramisu.Context(Id), ) -> #(Model, Effect(Msg), option.Option(_)) { let assert option.Some(physics_world) = ctx.physics_world case msg { Tick -> { let new_physics_world = physics.step(physics_world) #(model, effect.tick(Tick), option.Some(new_physics_world)) } } } fn view(_model: Model, ctx: tiramisu.Context(Id)) -> List(scene.Node(Id)) { let assert option.Some(physics_world) = ctx.physics_world let assert Ok(cam) = camera.perspective(field_of_view: 75.0, near: 0.1, far: 1000.0) let assert Ok(cube_geom) = geometry.box(width: 1.0, height: 1.0, depth: 1.0) let assert Ok(cube1_mat) = material.new() |> material.with_color(0xff4444) |> material.build let assert Ok(cube2_mat) = material.new() |> material.with_color(0x44ff44) |> material.build let assert Ok(ground_geom) = geometry.box(width: 20.0, height: 0.2, depth: 20.0) let assert Ok(ground_mat) = material.new() |> material.with_color(0x808080) |> material.build [ scene.Camera( id: Camera, camera: cam, transform: transform.at(position: vec3.Vec3(0.0, 10.0, 15.0)), look_at: option.Some(vec3.Vec3(0.0, 0.0, 0.0)), active: True, viewport: option.None, ), scene.Light( id: Ambient, light: { let assert Ok(light) = light.ambient(color: 0xffffff, intensity: 0.5) light }, transform: transform.identity, ), scene.Light( id: Directional, light: { let assert Ok(light) = light.directional(color: 0xffffff, intensity: 2.0) light }, transform: transform.at(position: vec3.Vec3(5.0, 10.0, 7.5)), ), // Ground (static physics body) scene.Mesh( id: Ground, geometry: ground_geom, material: ground_mat, transform: transform.at(position: vec3.Vec3(0.0, 0.0, 0.0)), physics: option.Some( physics.new_rigid_body(physics.Fixed) |> physics.with_collider(physics.Box(20.0, 0.2, 20.0)) |> physics.with_restitution(0.0) |> physics.build(), ), ), // Falling cube 1 (dynamic physics body) scene.Mesh( id: Cube1, geometry: cube_geom, material: cube1_mat, transform: case physics.get_transform(physics_world, Cube1) { Ok(t) -> t Error(Nil) -> transform.at(position: vec3.Vec3(-2.0, 5.0, 0.0)) }, physics: option.Some( physics.new_rigid_body(physics.Dynamic) |> physics.with_collider(physics.Box(1.0, 1.0, 1.0)) |> physics.with_mass(1.0) |> physics.with_restitution(0.5) |> physics.with_friction(0.5) |> physics.build(), ), ), // Falling cube 2 (dynamic physics body) scene.Mesh( id: Cube2, geometry: cube_geom, material: cube2_mat, transform: case physics.get_transform(physics_world, Cube2) { Ok(t) -> t Error(Nil) -> transform.at(position: vec3.Vec3(2.0, 7.0, 0.0)) }, physics: option.Some( physics.new_rigid_body(physics.Dynamic) |> physics.with_collider(physics.Box(1.0, 1.0, 1.0)) |> physics.with_mass(1.0) |> physics.with_restitution(0.6) |> physics.with_friction(0.3) |> physics.build(), ), ), ] } "/utf8>>. -file("src/mascarpone.gleam", 425). -spec create_main_file(binary(), template()) -> {ok, nil} | {error, snag:snag()}. create_main_file(Project_name, Template) -> Root = find_root(<<"."/utf8>>), Src_dir = filepath:join(Root, <<"src"/utf8>>), Main_path = filepath:join(Src_dir, <>), Content = case Template of two_d_game -> generate_2d_template(); three_d_game -> generate_3d_template(); physics_demo -> generate_physics_template() end, _pipe = simplifile:write(Main_path, Content), snag:map_error(_pipe, fun(_) -> <<"Could not write main file"/utf8>> end). -file("src/mascarpone.gleam", 270). -spec generate_project(model()) -> {ok, nil} | {error, snag:snag()}. generate_project(Model) -> gleam@result:'try'( update_gleam_toml(erlang:element(3, Model), erlang:element(4, Model)), fun(_) -> gleam@result:'try'( create_gitignore(), fun(_) -> gleam@result:'try'( create_main_file( erlang:element(3, Model), gleam@option:unwrap( erlang:element(6, Model), three_d_game ) ), fun(_) -> {ok, nil} end ) end ) end ). -file("src/mascarpone.gleam", 88). -spec update(model(), msg()) -> {model(), list(fun(() -> msg()))}. update(Model, Msg) -> case Msg of next_step -> Next_step = case erlang:element(2, Model) of welcome -> lustre_choice; lustre_choice -> template_choice; template_choice -> complete; complete -> complete; {failed, _} -> complete end, {{model, Next_step, erlang:element(3, Model), erlang:element(4, Model), erlang:element(5, Model), erlang:element(6, Model)}, []}; {set_lustre, Value} -> {{model, template_choice, erlang:element(3, Model), Value, erlang:element(5, Model), erlang:element(6, Model)}, []}; {set_template, Template} -> Updated_model = {model, erlang:element(2, Model), erlang:element(3, Model), erlang:element(4, Model), erlang:element(5, Model), {some, Template}}, Result = generate_project(Updated_model), case Result of {ok, _} -> {{model, complete, erlang:element(3, Updated_model), erlang:element(4, Updated_model), erlang:element(5, Updated_model), erlang:element(6, Updated_model)}, []}; {error, Err} -> {{model, {failed, snag:pretty_print(Err)}, erlang:element(3, Updated_model), erlang:element(4, Updated_model), erlang:element(5, Updated_model), erlang:element(6, Updated_model)}, []} end; generate -> Result@1 = generate_project(Model), case Result@1 of {ok, _} -> {{model, complete, erlang:element(3, Model), erlang:element(4, Model), erlang:element(5, Model), erlang:element(6, Model)}, []}; {error, Err@1} -> {{model, {failed, snag:pretty_print(Err@1)}, erlang:element(3, Model), erlang:element(4, Model), erlang:element(5, Model), erlang:element(6, Model)}, []} end end. -file("src/mascarpone.gleam", 16). -spec main() -> nil. main() -> Exit = gleam@erlang@process:new_subject(), case get_project_name() of {ok, Project_name} -> case begin _pipe = shore:spec( fun() -> init(Project_name) end, fun view/1, fun update/2, Exit, shore:default_keybinds(), shore:on_update() ), shore:start(_pipe) end of {ok, _} -> nil; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"mascarpone"/utf8>>, function => <<"main"/utf8>>, line => 21, value => _assert_fail, start => 398, 'end' => 672, pattern_start => 409, pattern_end => 419}) end, _pipe@1 = Exit, gleam_erlang_ffi:'receive'(_pipe@1); {error, Err} -> gleam_stdlib:println_error( <<"\n❌ Error: "/utf8, (snag:pretty_print(Err))/binary>> ) end.