bestguigui_lab 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 96f6d3fcc750e5d014248e718d7002117e19cdbe90b3787bc7709e2bf20e940c
4
+ data.tar.gz: 4f3e25a6ccade65ceb49178db07e71d70ace4d9afb81482a4bd19089b8ef8809
5
+ SHA512:
6
+ metadata.gz: 0512a0c2081412ef87f90d19690e38ba315eebd4ef51eb2f13047538384cf30f149db57bc46a216f30f1d37f3bd7a7800f630ca3ec0da9c7d9a32b5028b4f82b
7
+ data.tar.gz: 922ce4a677d389b45eb98272a260c83d620070da48f14979609e2738782f5ff61602a3b7750c19ccbd3f8677b54a520e3a3d816a621a8843b4eb2e03811ac871
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Charge et met en cache les assets (images, sons, modèles 3D...) pour éviter de les recharger plusieurs fois.
5
+ class Assets
6
+ @root = Dir.pwd
7
+
8
+ @images = Hash.new { |cache, path| cache[path] = Gosu::Image.new(path, retro: true) }
9
+ @sounds = Hash.new { |cache, path| cache[path] = Gosu::Sample.new(path) }
10
+ @songs = Hash.new { |cache, path| cache[path] = Gosu::Song.new(path) }
11
+ @fonts = Hash.new { |cache, key| cache[key] = Gosu::Font.new(key[1], name: key[0]) }
12
+ @models = Hash.new { |cache, path| cache[path] = ObjModel.new(path) }
13
+ @textures = Hash.new { |cache, path| cache[path] = GLTexture.new(path) }
14
+
15
+ def self.resolve(name) = File.expand_path(name, @root)
16
+ def self.image(name) = @images[resolve(name)]
17
+ def self.sound(name) = @sounds[resolve(name)]
18
+ def self.song(name) = @songs[resolve(name)]
19
+
20
+ def self.font(name = Gosu.default_font_name, size: 20)
21
+ path = name == Gosu.default_font_name ? name : resolve(name)
22
+ @fonts[[path, size]]
23
+ end
24
+
25
+ def self.model(name) = @models[resolve(name)]
26
+ def self.texture(name) = @textures[resolve(name)]
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Texture OpenGL indépendante de l'atlas partagé de Gosu, construite à
5
+ # partir des pixels bruts (`to_blob`) — permet un vrai GL_REPEAT natif.
6
+ class GLTexture
7
+ attr_reader :tex_name
8
+
9
+ def initialize(path)
10
+ super()
11
+ image = Gosu::Image.new(path, retro: true)
12
+ @tex_name = upload(image)
13
+ end
14
+
15
+ private
16
+
17
+ def upload(image)
18
+ name_buffer = "\x00" * 4
19
+ glGenTextures(1, name_buffer)
20
+ tex_name = name_buffer.unpack1('L')
21
+
22
+ # Garder les pixels dans une variable (pas `image.to_blob` inline
23
+ # en argument) : sinon Ruby peut libérer la String pendant
24
+ # l'appel FFI, avant qu'OpenGL l'ait lue (texture à zéro, sans
25
+ # erreur OpenGL).
26
+ pixels = image.to_blob
27
+
28
+ glBindTexture(GL_TEXTURE_2D, tex_name)
29
+ configure_wrapping
30
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels)
31
+
32
+ tex_name
33
+ end
34
+
35
+ def configure_wrapping
36
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
37
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
38
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
39
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Petites fonctions utilitaires pour les animations (interpolation, easing).
5
+ class Helpers
6
+ def self.lerp(from, dest, progress) = from + ((dest - from) * progress)
7
+
8
+ def self.ease_out(progress) = 1 - ((1 - progress)**2)
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Mappe une action à plusieurs touches/boutons (ex. right: [KB_RIGHT, KB_D]).
5
+ class Input
6
+ def initialize(bindings = {})
7
+ @bindings = bindings
8
+ end
9
+
10
+ def button_down?(action) = @bindings[action].any? { |key| Gosu.button_down?(key) }
11
+ def button_down(action, id) = @bindings[action].any? { |key| key == id }
12
+ def button_up(action, id) = @bindings[action].any? { |key| key == id }
13
+ end
14
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Charge un modèle .obj/.mtl et le dessine via un VBO : géométrie
5
+ class ObjModel
6
+ # position (3) + texcoord (2) + normale (3) par sommet.
7
+ FLOATS_PER_VERTEX = 8
8
+ STRIDE = FLOATS_PER_VERTEX * 4 # 4 octets par float
9
+
10
+ def initialize(obj_path)
11
+ super()
12
+ @vertices = []
13
+ @tex_coords = []
14
+ @normals = []
15
+ @faces = []
16
+ @texture = nil
17
+
18
+ parse_obj(obj_path)
19
+ @vertex_count = @faces.size
20
+ @vbo = build_vertex_buffer
21
+ end
22
+
23
+ def draw
24
+ glEnable(GL_TEXTURE_2D)
25
+ glBindTexture(GL_TEXTURE_2D, @texture.tex_name)
26
+ glBindBuffer(GL_ARRAY_BUFFER, @vbo)
27
+
28
+ enable_vertex_arrays
29
+ glDrawArrays(GL_TRIANGLES, 0, @vertex_count)
30
+ disable_vertex_arrays
31
+
32
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
33
+ glDisable(GL_TEXTURE_2D)
34
+ end
35
+
36
+ private
37
+
38
+ def parse_obj(path)
39
+ directory = File.dirname(path)
40
+
41
+ File.foreach(path) do |line|
42
+ tokens = line.split
43
+ next if tokens.empty?
44
+
45
+ keyword = tokens.shift
46
+ parse_line(keyword, tokens, directory)
47
+ end
48
+ end
49
+
50
+ def parse_line(keyword, tokens, directory)
51
+ case keyword
52
+ when 'v', 'vt', 'vn' then parse_coordinates(keyword, tokens)
53
+ when 'f' then @faces.concat(parse_face(tokens))
54
+ when 'mtllib' then load_material(File.join(directory, tokens.first))
55
+ end
56
+ end
57
+
58
+ def parse_coordinates(keyword, tokens)
59
+ values = tokens.map(&:to_f)
60
+
61
+ case keyword
62
+ when 'v' then @vertices << values
63
+ when 'vt' then @tex_coords << values
64
+ when 'vn' then @normals << values
65
+ end
66
+ end
67
+
68
+ # "f" contient des groupes "v/vt/vn" (indices 1-indexés, vt/vn
69
+ # parfois absents).
70
+ def parse_face(tokens)
71
+ tokens.map do |group|
72
+ v, vt, vn = group.split('/').map { |i| i.nil? || i.empty? ? nil : i.to_i - 1 }
73
+ [v, vt, vn]
74
+ end
75
+ end
76
+
77
+ # Un seul matériau par modèle : la dernière texture (map_Kd) trouvée.
78
+ def load_material(path)
79
+ directory = File.dirname(path)
80
+
81
+ File.foreach(path) do |line|
82
+ tokens = line.split
83
+ next if tokens.empty? || tokens.shift != 'map_Kd'
84
+
85
+ @texture = GLTexture.new(File.join(directory, tokens.first))
86
+ end
87
+ end
88
+
89
+ # Buffer plat non indexé : les sommets partagés sont dupliqués, pas
90
+ # d'index buffer pour rester simple.
91
+ def build_vertex_buffer
92
+ data = @faces.flat_map { |v, vt, vn| vertex_attributes(v, vt, vn) }.pack('F*')
93
+
94
+ name_buffer = "\x00" * 4
95
+ glGenBuffers(1, name_buffer)
96
+ vbo = name_buffer.unpack1('L')
97
+
98
+ glBindBuffer(GL_ARRAY_BUFFER, vbo)
99
+ glBufferData(GL_ARRAY_BUFFER, data.bytesize, data, GL_STATIC_DRAW)
100
+
101
+ vbo
102
+ end
103
+
104
+ def vertex_attributes(vertex_index, tex_coord_index, normal_index)
105
+ position = @vertices[vertex_index]
106
+ tex_coord = tex_coord_index ? @tex_coords[tex_coord_index] : [0.0, 0.0]
107
+ normal = normal_index ? @normals[normal_index] : [0.0, 0.0, 1.0]
108
+
109
+ position + tex_coord + normal
110
+ end
111
+
112
+ def enable_vertex_arrays
113
+ glEnableClientState(GL_VERTEX_ARRAY)
114
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY)
115
+ glEnableClientState(GL_NORMAL_ARRAY)
116
+
117
+ glVertexPointer(3, GL_FLOAT, STRIDE, 0)
118
+ glTexCoordPointer(2, GL_FLOAT, STRIDE, 3 * 4)
119
+ glNormalPointer(GL_FLOAT, STRIDE, 5 * 4)
120
+ end
121
+
122
+ def disable_vertex_arrays
123
+ glDisableClientState(GL_NORMAL_ARRAY)
124
+ glDisableClientState(GL_TEXTURE_COORD_ARRAY)
125
+ glDisableClientState(GL_VERTEX_ARRAY)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Instancie-la une fois, garde une référence, utilise-la dans draw :
5
+ # @renderer = Render3D.new
6
+ # @renderer.render_3d(self) { @renderer.look_at(...) ; @model.draw }
7
+ class Render3D
8
+ # fov : angle de champ de vision vertical, en degrés.
9
+ # clip : [proche, lointain] — tout ce qui est en dehors n'est pas dessiné.
10
+ def render_3d(window, z_order: 0, fov: 60.0, clip: [0.1, 1000.0], &block)
11
+ w, h = window.fullscreen? ? [Gosu.screen_width, Gosu.screen_height] : [window.width, window.height]
12
+ Gosu.gl(z_order) { render_3d_frame(w, h, fov, clip, &block) }
13
+ end
14
+
15
+ # À appeler dans le bloc de render_3d, avant de dessiner, pour
16
+ # positionner la caméra : eye = position, center = point regardé.
17
+ def look_at(eye, center, up_vector = [0, 1, 0])
18
+ gluLookAt(*eye, *center, *up_vector)
19
+ end
20
+
21
+ private
22
+
23
+ # Gosu.gl isole déjà l'état (glPushAttrib/glPopAttrib) et remet sa
24
+ # propre projection/modelview en sortant du bloc — pas besoin de
25
+ # push/pop matrice de notre côté, juste de poser nos valeurs.
26
+ def render_3d_frame(width, height, fov, clip)
27
+ glEnable(GL_DEPTH_TEST)
28
+ glDepthFunc(GL_LESS)
29
+
30
+ setup_projection(width, height, fov, clip)
31
+ glMatrixMode(GL_MODELVIEW)
32
+ glLoadIdentity
33
+
34
+ yield
35
+ end
36
+
37
+ def setup_projection(width, height, fov, clip)
38
+ near, far = clip
39
+
40
+ glMatrixMode(GL_PROJECTION)
41
+ glLoadIdentity
42
+ gluPerspective(fov, width.to_f / height, near, far)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Classe de base pour une scène (écran de jeu, menu, victoire...).
5
+ # Hérite-en et ne redéfinis que ce dont tu as besoin.
6
+ class Scene
7
+ def initialize(window)
8
+ @window = window
9
+ @font = Assets.font(size: 24)
10
+ end
11
+
12
+ def button_down(id); end
13
+
14
+ def update(dt); end
15
+
16
+ def draw; end
17
+ end
18
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ # Le rubis animé du splash screen : tombe du haut de l'écran, touche le
5
+ # titre, rebondit au-dessus, puis se stabilise. Dessiné en primitives
6
+ # Gosu (aucune image).
7
+ class SplashGem
8
+ SIZE = 220
9
+ GAP = 40
10
+ OVERSHOOT = 12
11
+ FALL = 0.5
12
+ BOUNCE_UP = 0.25
13
+ SETTLE = 0.2
14
+ IMPACT_DIP = 10
15
+
16
+ # Points du diamant, en fractions (0..1) d'une boîte carrée.
17
+ POINTS = {
18
+ tl: [0.30, 0.32],
19
+ tr: [0.70, 0.32],
20
+ l: [0.12, 0.45],
21
+ r: [0.88, 0.45],
22
+ hub: [0.50, 0.47],
23
+ bottom: [0.50, 0.88]
24
+ }.freeze
25
+
26
+ HIGHLIGHT = [240, 150, 155].freeze
27
+ BASE = [210, 90, 95].freeze
28
+ DARK = [180, 60, 65].freeze
29
+ OUTLINE_RGB = [40, 30, 40].freeze
30
+
31
+ FACETS = [
32
+ [%i[tl hub l], HIGHLIGHT],
33
+ [%i[tl tr hub], BASE],
34
+ [%i[tr r hub], DARK],
35
+ [%i[l hub bottom], HIGHLIGHT],
36
+ [%i[r bottom hub], DARK]
37
+ ].freeze
38
+
39
+ OUTLINE = %i[tl tr r bottom l].freeze
40
+
41
+ def initialize(window)
42
+ @window = window
43
+ end
44
+
45
+ def draw(elapsed, alpha)
46
+ center_x = @window.width / 2
47
+ center_y = center_y(elapsed)
48
+
49
+ FACETS.each { |names, rgb| draw_facet(names, rgb, center_x, center_y, alpha) }
50
+ draw_outline(center_x, center_y, alpha)
51
+ end
52
+
53
+ def title_offset(elapsed)
54
+ return 0 if elapsed < FALL
55
+
56
+ if elapsed < FALL + BOUNCE_UP
57
+ Helpers.lerp(IMPACT_DIP, 0, Helpers.ease_out((elapsed - FALL) / BOUNCE_UP))
58
+ else
59
+ 0
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def center_y(elapsed)
66
+ if elapsed < FALL
67
+ fall_y(elapsed)
68
+ elsif elapsed < FALL + BOUNCE_UP
69
+ bounce_up_y(elapsed)
70
+ elsif elapsed < FALL + BOUNCE_UP + SETTLE
71
+ settle_y(elapsed)
72
+ else
73
+ rest_y
74
+ end
75
+ end
76
+
77
+ def impact_y = (@window.height / 2) - bottom_offset
78
+
79
+ def bottom_offset = (POINTS[:bottom][1] - 0.6) * SIZE
80
+
81
+ def rest_y = impact_y - GAP
82
+
83
+ def top_y = rest_y - OVERSHOOT
84
+
85
+ def fall_y(elapsed) = Helpers.lerp(-SIZE, impact_y, Helpers.ease_out(elapsed / FALL))
86
+
87
+ def bounce_up_y(elapsed)
88
+ progress = Helpers.ease_out((elapsed - FALL) / BOUNCE_UP)
89
+ Helpers.lerp(impact_y, top_y, progress)
90
+ end
91
+
92
+ def settle_y(elapsed)
93
+ progress = Helpers.ease_out((elapsed - FALL - BOUNCE_UP) / SETTLE)
94
+ Helpers.lerp(top_y, rest_y, progress)
95
+ end
96
+
97
+ def draw_facet(names, rgb, center_x, center_y, alpha)
98
+ color = Gosu::Color.new(alpha, *rgb)
99
+ a, b, c = names.map { |name| point(name, center_x, center_y) }
100
+ Gosu.draw_triangle(a[0], a[1], color, b[0], b[1], color, c[0], c[1], color)
101
+ end
102
+
103
+ def draw_outline(center_x, center_y, alpha)
104
+ color = Gosu::Color.new(alpha, *OUTLINE_RGB)
105
+ points = OUTLINE.map { |name| point(name, center_x, center_y) }
106
+ points.each_with_index do |p, i|
107
+ next_p = points[(i + 1) % points.size]
108
+ Gosu.draw_line(p[0], p[1], color, next_p[0], next_p[1], color)
109
+ end
110
+ end
111
+
112
+ def point(name, center_x, center_y)
113
+ fx, fy = POINTS[name]
114
+ [center_x + ((fx - 0.5) * SIZE), center_y + ((fy - 0.6) * SIZE)]
115
+ end
116
+ end
117
+
118
+ # Écran de démarrage prêt à l'emploi : titre + "Powered by Gosu" en fondu,
119
+ # avec un SplashGem qui tombe et rebondit sur le titre. Bascule ensuite
120
+ # sur la scène suivante.
121
+ #
122
+ # Bestguigui::SplashScene.new(window, title: "Mon jeu", next_scene: :menu)
123
+ class SplashScene < Scene
124
+ FADE_IN = 0.6
125
+ HOLD = 1.2
126
+ FADE_OUT = 0.6
127
+
128
+ def initialize(window, title:, next_scene:)
129
+ super(window)
130
+ @title = title
131
+ @next_scene = next_scene
132
+ @gem = SplashGem.new(window)
133
+ @title_font = Assets.font(size: 64)
134
+ @subtitle_font = Assets.font(size: 24)
135
+ @gosu_font = Assets.font(size: 36)
136
+ @elapsed = 0.0
137
+ end
138
+
139
+ def update(dt)
140
+ @elapsed += dt / 1000.0
141
+ @window.switch_scene(@next_scene) if @elapsed >= FADE_IN + HOLD + FADE_OUT
142
+ end
143
+
144
+ def draw
145
+ color = Gosu::Color.new(fade_alpha, 255, 255, 255)
146
+ accent = Gosu::Color.new(fade_alpha, 220, 30, 40)
147
+ offset = @gem.title_offset(@elapsed)
148
+
149
+ @gem.draw(@elapsed, fade_alpha)
150
+ draw_centered(@title_font, @title, (@window.height / 2) + offset, color)
151
+ draw_powered_by((@window.height / 2) + 80 + offset, color, accent)
152
+ end
153
+
154
+ private
155
+
156
+ def draw_powered_by(top, color, accent)
157
+ prefix_width = @subtitle_font.text_width('Powered by ')
158
+ total_width = prefix_width + @gosu_font.text_width('Gosu')
159
+ x = (@window.width - total_width) / 2
160
+
161
+ @subtitle_font.draw_text('Powered by ', x, top + 6, 0, 1, 1, color)
162
+ @gosu_font.draw_text('Gosu', x + prefix_width, top, 0, 1, 1, accent)
163
+ end
164
+
165
+ def fade_alpha
166
+ if @elapsed < FADE_IN
167
+ (255 * (@elapsed / FADE_IN)).to_i
168
+ elsif @elapsed < FADE_IN + HOLD
169
+ 255
170
+ else
171
+ remaining = FADE_IN + HOLD + FADE_OUT - @elapsed
172
+ (255 * (remaining / FADE_OUT)).clamp(0, 255).to_i
173
+ end
174
+ end
175
+
176
+ def draw_centered(font, text, top, color)
177
+ x = (@window.width - font.text_width(text)) / 2
178
+ font.draw_text(text, x, top, 0, 1, 1, color)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bestguigui
4
+ VERSION = '0.4.0'
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gosu'
4
+ require 'opengl'
5
+ require 'glu'
6
+
7
+ include OpenGL
8
+ include GLU
9
+
10
+ # Sans ça, les fonctions OpenGL au-delà de la version 1.1 plantent en
11
+ # silence (NoMethodError sur nil).
12
+ OpenGL.load_lib
13
+ GLU.load_lib
14
+
15
+ Dir[File.join(__dir__, 'bestguigui_lab', '*.rb')].each { |fn| require_relative fn }
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bestguigui_lab
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Bestguigui
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: gosu
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.4'
26
+ - !ruby/object:Gem::Dependency
27
+ name: opengl-bindings
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.6'
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - lib/bestguigui_lab.rb
45
+ - lib/bestguigui_lab/assets.rb
46
+ - lib/bestguigui_lab/gl_texture.rb
47
+ - lib/bestguigui_lab/helpers.rb
48
+ - lib/bestguigui_lab/input.rb
49
+ - lib/bestguigui_lab/obj_model.rb
50
+ - lib/bestguigui_lab/render_3d.rb
51
+ - lib/bestguigui_lab/scene.rb
52
+ - lib/bestguigui_lab/splash_scene.rb
53
+ - lib/bestguigui_lab/version.rb
54
+ homepage: https://github.com/guillaumequillet/bestguigui_lab
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 4.0.16
74
+ specification_version: 4
75
+ summary: A Game Jam Starter Kit to optimize Game Jams
76
+ test_files: []