Files
project_misth/samples/main.rb
T
2026-06-04 23:46:34 +01:00

186 lines
7.2 KiB
Ruby

# Sample ruby DSL to create a game window.
# reference of what the engine should build up to.
width, height = 720, 480
image = Tiles.from("assets/loading.png").first
App.run width, height, title: "My Game", loading_bg: image # can be image or animation
App.map_key ?w, :forward
App.map_key ?s, :backward
App.map_key ?a, :left
App.map_key ?d, :right
App.map_key ?h, :heal
App.map_key ?t, :place
menu_scene = Scene.new
default_font = Font.new "assets/DejaVuSans.ttf", 12, italic: true
start_button = ElementText.new "Start Game", font: default_font, position: {x: 100, y: 100}
game_scene = Scene.new
start_button.on_click do
App.switch_to game_scene
end
menu_scene << start_button
world = ElementWorld.new
# world.cam # camera object to control the view of the world, has position, zoom, rotation
# can define any components with name and groups it is part of, systems will apply behaviour based on their groups
# there are 5 inbuilt groups that have systems written for them, but you can create more components and systems for whatever you want
# this is a custom component
world.component :health do |c|
field :health, int: 100
allow_redefine [:health]
end
world.spawn do |e| # Name is optional
e.tags = [:enemy]
e.position = {x: 100, y: 100} # entities always have a position, even if they don't have a body component, this is useful for rendering and for entities that don't need physics but still need to be positioned in the world
e.add_component :body do |c|
c.shape = :rectangle
c.size = {width: 50, height: 50}
c.angle = 0
c.mass = 1
c.velocity = {x: 0, y: 0}
c.collision = true
end
e.add_component :sprite do |c|
tiles = Tiles.from "assets/sprite.png", tile_width: 50, tile_height: 50
animation1 = {tiles: tiles[0..3], frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
## For tilemaps
# tiles is an array of of tiles in sheet, largest one's w and h will be used. or width: height: to override
c.tilemap = Tilemap.new tiles
c.default_tile = tiles.first # default tile to use for empty spaces in the tilemap
c.auto_tile = true # tiles must follow pattern
# c.tile[x: 0, y: 0] = tiles[0] # set tile at position x,y in tilemap to a specific tile from the sheet (if no auto_tile is used)
c.tile[x: 0, y: 0] = true # true/false otherwise
end
e.add_component :health do |c|
c.health = 100
end
end
world.spawn "Trap Preview" do |e|
e.position = {x: 0, y: 0}
e.tags = [:trap_preview]
e.active = false
e.add_component :sprite do |c|
tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50
animation1 = {tiles: tiles[0..3], frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
end
# e.remove_component :sprite # compoenents can also be removed
end
# systems are called on the world every update frame, some systems are built into the engine for things like physics, rendering, etc, but you can also create your own custom systems for things like trap placement, enemy spawning, etc
# systems can be created on queries
# the query can be for compoenents or tags or both
# match is set to list of all capturing queries
# for components returns an object that responds to .each with every ceomponents values
# for entities it does similarly
# mouse state can be read in systems always
query = Query.from(tag(:enemy) & component(:health, true) & anti(name("Trap Preview", true)))
# in the match name will return just the one entity
# tag and componet return list-like tables but they will be parallel and in order
# empty columns will be filled with nil (not possible if only & is used)
# the last part is to show how anti() works
# it doesnt actually do anything as tag(:enemy) and component(:health) will never match name("Trap Preview") but it shows how you can use anti() to negate a query
world.system :health_regen, for: query do |match, dt| # no trigger means always
# example of a simple health regeneration system that regenerates health for all entities with a health component
healths = query.get[0] # get the first component in the query, in this case health, this will be a list of all health components for entities that match the query
healths.add_scalar! :health, 1 * dt
healths.clamp! :health, 0, 100
end
trigger = [:place]
world.system :trap_placement, trigger: trigger do |dt|
return unless world.holding_trap?
world.spawn do |e|
e.tags = [:trap]
e.position = world.mouse_position_world
e.add_component :sprite do |c|
tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50
tiles2 = Tiles.from "assets/trap_2.png", tile_width: 50, tile_height: 50
animation1 = {tiles: tiles[0..3] + tiles2[0..3], frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
end
e.add_component :body do |c|
c.shape = :rectangle
c.size = {width: 50, height: 50}
c.angle = 0
c.mass = 1
c.velocity = {x: 0, y: 0}
c.collision = true
end
end
end
name_query = Query.from(name("Trap Preview", true) & component(:sprite, true))
# could be done as so or
world.system :trap_preview do |match, dt| # example from teh game mist trying to be made with this engine
# do trap placement logic when a trap is held
return unless world.holding_trap?
# show a preview of the trap at the mouse position, you can use the mouse position relative to the world for this
e = name_query.get[0] # get the first entity that matches the name query, in this case the trap preview entity, this will be nil if no entity matches
e.position = world.mouse_position_world
e.active = true # enable the trap preview entity to show it at the mouse position
end
# OR, this one isnt the exact same but shows that .active works on both entites and components
world.system :trap_preview, for: name("Trap Preview", true) & component(:sprite, true) do |match, dt|
return unless world.holding_trap?
# if | was used between teh queries then the arrays would still be parallel but empty rows fit with nil
# for & between they will always match and so the arrays will always be the same length and so you can just use the first one to set the position of the entity and then use the second one to set the active of the sprite component, etc (without nil check)
match.second.each_with_index do |s, i|
match.first[i].position = world.mouse_position_world
s.active = true
end
end
## ------ ##
world.shader :fog do |shader|
# shaders are per world
vertex "assets/fog.vert"
fragment "assets/fog.frag"
data_format = {
name: :float,
time: :float,
resolution: :vec2,
# data required by teh shader is defined here once
}
frame do |shader|
# example of custom shader code for this shader, this is called every frame and is where you would update the shader data based on the current state of the world or whatever
shader.set_data :time, Time.now.to_f
shader.set_data :resolution, {x: width, y: height}
end
z = 100 # shaders always appear above normal sprites but z is used to sort them relative to each other, so a shader with z 100 will appear above a shader with z 50, but both will appear above normal sprites
end
App.start menu_scene # first scene to show when the game starts, stops the loading bg