diff --git a/assets/fira_code.ttf b/assets/fira_code.ttf
new file mode 100644
index 0000000..0d57068
Binary files /dev/null and b/assets/fira_code.ttf differ
diff --git a/html/assets/fira_code.ttf b/html/assets/fira_code.ttf
new file mode 100644
index 0000000..0d57068
Binary files /dev/null and b/html/assets/fira_code.ttf differ
diff --git a/html/fishing-minigame.html b/html/fishing-minigame.html
new file mode 100644
index 0000000..05bf416
--- /dev/null
+++ b/html/fishing-minigame.html
@@ -0,0 +1,27 @@
+
+
+
+
+ TITLE
+
+
+
+
+
+
+
+
+
+
+
diff --git a/html/fishing-minigame.wasm b/html/fishing-minigame.wasm
new file mode 100755
index 0000000..62589f2
Binary files /dev/null and b/html/fishing-minigame.wasm differ
diff --git a/html/mq_js_bundle.js b/html/mq_js_bundle.js
new file mode 100644
index 0000000..3334c49
--- /dev/null
+++ b/html/mq_js_bundle.js
@@ -0,0 +1 @@
+var clipboard,plugins,wasm_memory,high_dpi,FS,GL,Module,wasm_exports,emscripten_shaders_hack,importObject,ctx,quad_socket,connected,received_buffer,uid,ongoing_requests;"use strict";const version="0.1.24",canvas=document.querySelector("#glcanvas"),gl=canvas.getContext("webgl");gl===null&&alert("Unable to initialize WebGL. Your browser or machine may not support it."),clipboard=null,plugins=[];high_dpi=!1,canvas.focus(),canvas.requestPointerLock=canvas.requestPointerLock||canvas.mozRequestPointerLock||function(){},document.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock||function(){};function assert(a,b){a==!1&&alert(b)}function acquireVertexArrayObjectExtension(a){var b=a.getExtension('OES_vertex_array_object');b?(a.createVertexArray=function(){return b.createVertexArrayOES()},a.deleteVertexArray=function(a){b.deleteVertexArrayOES(a)},a.bindVertexArray=function(a){b.bindVertexArrayOES(a)},a.isVertexArray=function(a){return b.isVertexArrayOES(a)}):alert("Unable to get OES_vertex_array_object extension")}function acquireInstancedArraysExtension(a){var b=a.getExtension('ANGLE_instanced_arrays');b&&(a.vertexAttribDivisor=function(a,c){b.vertexAttribDivisorANGLE(a,c)},a.drawArraysInstanced=function(a,c,d,e){b.drawArraysInstancedANGLE(a,c,d,e)},a.drawElementsInstanced=function(a,c,d,e,f){b.drawElementsInstancedANGLE(a,c,d,e,f)})}function acquireDisjointTimerQueryExtension(a){var b=a.getExtension('EXT_disjoint_timer_query');b&&(a.createQuery=function(){return b.createQueryEXT()},a.beginQuery=function(a,c){return b.beginQueryEXT(a,c)},a.endQuery=function(a){return b.endQueryEXT(a)},a.deleteQuery=function(a){b.deleteQueryEXT(a)},a.getQueryObject=function(a,c){return b.getQueryObjectEXT(a,c)})}acquireVertexArrayObjectExtension(gl),acquireInstancedArraysExtension(gl),acquireDisjointTimerQueryExtension(gl),gl.getExtension('WEBGL_depth_texture')==null&&alert("Cant initialize WEBGL_depth_texture extension");function getArray(a,b,c){return new b(wasm_memory.buffer,a,c)}function UTF8ToString(i,j){var b,h,c,a,d,f,g;let e=new Uint8Array(wasm_memory.buffer,i);for(b=0,h=b+j,c='';!(b>=h);){if(a=e[b++],!a)return c;if(!(a&128)){c+=String.fromCharCode(a);continue}if(d=e[b++]&63,(a&224)==192){c+=String.fromCharCode((a&31)<<6|d);continue}f=e[b++]&63,(a&240)==224?a=(a&15)<<12|d<<6|f:((a&248)!=240&&console.warn('Invalid UTF-8 leading byte 0x'+a.toString(16)+' encountered when deserializing a UTF-8 string on the asm.js/wasm heap to a JS string!'),a=(a&7)<<18|d<<12|f<<6|e[b++]&63),a<65536?c+=String.fromCharCode(a):(g=a-65536,c+=String.fromCharCode(55296|g>>10,56320|g&1023))}return c}function stringToUTF8(f,c,b,g){for(var h=b,e=b+g,d=0,a,i;d=55296&&a<=57343&&(i=f.charCodeAt(++d),a=65536+((a&1023)<<10)|i&1023),a<=127){if(b>=e)break;c[b++]=a}else if(a<=2047){if(b+1>=e)break;c[b++]=192|a>>6,c[b++]=128|a&63}else if(a<=65535){if(b+2>=e)break;c[b++]=224|a>>12,c[b++]=128|a>>6&63,c[b++]=128|a&63}else{if(b+3>=e)break;a>=2097152&&console.warn('Invalid Unicode code point 0x'+a.toString(16)+' encountered when serializing a JS string to an UTF-8 string on the asm.js/wasm heap! (Valid unicode code points should be in range 0-0x1FFFFF).'),c[b++]=240|a>>18,c[b++]=128|a>>12&63,c[b++]=128|a>>6&63,c[b++]=128|a&63}return b-h}FS={loaded_files:[],unique_id:0},GL={counter:1,buffers:[],mappedBuffers:{},programs:[],framebuffers:[],renderbuffers:[],textures:[],uniforms:[],shaders:[],vaos:[],timerQueries:[],contexts:{},programInfos:{},getNewId:function(b){for(var c=GL.counter++,a=b.length;a=0&&b=GL.counter){console.error("GL_INVALID_VALUE in glGetProgramiv");return}if(e=GL.programInfos[a],!e){console.error('GL_INVALID_OPERATION in glGetProgramiv(program='+a+', pname='+b+', p=0x'+c.toString(16)+'): The specified GL object name does not refer to a program object!');return}if(b==35716)d=gl.getProgramInfoLog(GL.programs[a]),assert(d!==null),getArray(c,Int32Array,1)[0]=d.length+1;else if(b==35719){console.error("unsupported operation");return}else if(b==35722){console.error("unsupported operation");return}else if(b==35381){console.error("unsupported operation");return}else getArray(c,Int32Array,1)[0]=gl.getProgramParameter(GL.programs[a],b)},glCreateShader:function(b){var a=GL.getNewId(GL.shaders);return GL.shaders[a]=gl.createShader(b),a},glStencilFuncSeparate:function(a,b,c,d){gl.stencilFuncSeparate(a,b,c,d)},glStencilMaskSeparate:function(a,b){gl.stencilMaskSeparate(a,b)},glStencilOpSeparate:function(a,b,c,d){gl.stencilOpSeparate(a,b,c,d)},glFrontFace:function(a){gl.frontFace(a)},glCullFace:function(a){gl.cullFace(a)},glCopyTexImage2D:function(a,b,c,d,e,f,g,h){gl.copyTexImage2D(a,b,c,d,e,f,g,h)},glShaderSource:function(b,d,e,f){var a,c;GL.validateGLObjectID(GL.shaders,b,'glShaderSource','shader'),a=GL.getSource(b,d,e,f),emscripten_shaders_hack&&(a=a.replace(/#extension GL_OES_standard_derivatives : enable/g,""),a=a.replace(/#extension GL_EXT_shader_texture_lod : enable/g,''),c='',a.indexOf('gl_FragColor')!=-1&&(c+='out mediump vec4 GL_FragColor;\n',a=a.replace(/gl_FragColor/g,'GL_FragColor')),a.indexOf('attribute')!=-1?(a=a.replace(/attribute/g,'in'),a=a.replace(/varying/g,'out')):a=a.replace(/varying/g,'in'),a=a.replace(/textureCubeLodEXT/g,'textureCubeLod'),a=a.replace(/texture2DLodEXT/g,'texture2DLod'),a=a.replace(/texture2DProjLodEXT/g,'texture2DProjLod'),a=a.replace(/texture2DGradEXT/g,'texture2DGrad'),a=a.replace(/texture2DProjGradEXT/g,'texture2DProjGrad'),a=a.replace(/textureCubeGradEXT/g,'textureCubeGrad'),a=a.replace(/textureCube/g,'texture'),a=a.replace(/texture1D/g,'texture'),a=a.replace(/texture2D/g,'texture'),a=a.replace(/texture3D/g,'texture'),a=a.replace(/#version 100/g,'#version 300 es\n'+c)),gl.shaderSource(GL.shaders[b],a)},glGetProgramInfoLog:function(d,c,g,e){var b,a;GL.validateGLObjectID(GL.programs,d,'glGetProgramInfoLog','program'),b=gl.getProgramInfoLog(GL.programs[d]),assert(b!==null);let f=getArray(e,Uint8Array,c);for(a=0;a>24&255,c=a>>16&255,d=a&65535;return b+"."+c+"."+d}function init_plugins(b){var a,c,d;if(b==void 0)return;for(a=0;a(add_missing_functions_stabs(a),WebAssembly.instantiate(a,importObject))).then(a=>{wasm_memory=a.exports.memory,wasm_exports=a.exports;var b=u32_to_semver(wasm_exports.crate_version());version!=b&&console.error("Version mismatch: gl.js version is: "+version+", rust sapp-wasm crate version is: "+b),init_plugins(plugins),a.exports.main()}).catch(a=>{console.error("WASM failed to load, probably incompatible gl.js version"),console.error(a)}):a.then(function(a){return a.arrayBuffer()}).then(function(a){return WebAssembly.compile(a)}).then(function(a){return add_missing_functions_stabs(a),WebAssembly.instantiate(a,importObject)}).then(function(a){wasm_memory=a.exports.memory,wasm_exports=a.exports;var b=u32_to_semver(wasm_exports.crate_version());version!=b&&console.error("Version mismatch: gl.js version is: "+version+", rust sapp-wasm crate version is: "+b),init_plugins(plugins),a.exports.main()}).catch(a=>{console.error("WASM failed to load, probably incompatible gl.js version"),console.error(a)})}const AudioContext=window.AudioContext||window.webkitAudioContext;let audio_context,sounds={},audio_next_handle=1;function audio_init(){if(audio_context==null){audio_context=new AudioContext,audio_listener=audio_context.listener;let a=audio_context.createBufferSource();a.buffer=audio_context.createBuffer(1,1,22050),a.connect(audio_context.destination),a.start(0)}}function audio_add_buffer(b,c){let d=wasm_memory.buffer.slice(b,b+c),a=audio_next_handle;return audio_next_handle+=1,sounds[a]={},audio_context.decodeAudioData(d,function(b){sounds[a].buffer=b},function(a){console.error("Failed to decode audio buffer",a)}),a}function audio_source_is_loaded(a){return a in sounds&&sounds[a].buffer!=void 0}function audio_play_buffer(f,g,h,j,i){audio_source_stop(f);var d=sounds[f];let a=audio_context.createBufferSource();a.loop=i;let b=null,c=null,e=null;if(g!=1||h!=1){b=audio_context.createGain(),a.connect(b),c=audio_context.createGain(),a.connect(c);let e=audio_context.createChannelMerger(2);b.connect(e,0,0),c.connect(e,0,1),e.connect(audio_context.destination),b.gain.value=g,c.gain.value=h,d.merger=e}else a.connect(audio_context.destination);a.playbackRate.value=j,d.source=a,d.gains=[b,c],a.onended=function(){a.disconnect(),b&&b.disconnect(),c&&c.disconnect(),e&&e.disconnect()},a.buffer=d.buffer;try{a.start(0)}catch(a){console.error("Error starting sound",a)}}function audio_source_set_volume(a,d,e){if(!(a in sounds))return;let b=sounds[a].gains,c=audio_context.currentTime+1/120;b[0].gain.linearRampToValueAtTime(d,c),b[1].gain.linearRampToValueAtTime(e,c)}function audio_source_stop(a){if(!(a in sounds)){console.log("stopping already remvoed sound");return}if(sounds[a].source==void 0)return;try{sounds[a].source.stop(),sounds[a].source.disconnect();for(node of sounds[a].gains)node!=null&&node.disconnect();sounds[a].merger&&sounds[a].merger.disconnect()}catch(a){console.error("Error stopping sound",a)}delete sounds[a].source,delete sounds[a].gains}function register_plugin(a){a.env.audio_init=audio_init,a.env.audio_add_buffer=audio_add_buffer,a.env.audio_play_buffer=audio_play_buffer,a.env.audio_source_is_loaded=audio_source_is_loaded,a.env.audio_source_set_volume=audio_source_set_volume,a.env.audio_source_stop=audio_source_stop}miniquad_add_plugin({register_plugin,on_init,version:"0.1.0",name:"macroquad_audio"}),ctx=null,js_objects={},unique_js_id=0,register_plugin=function(a){a.env.js_create_string=function(a,b){var c=UTF8ToString(a,b);return js_object(c)},a.env.js_create_buffer=function(c,d){var a=new Uint8Array(wasm_memory.buffer,c,d),b=new Uint8Array(new ArrayBuffer(a.byteLength));return b.set(new Uint8Array(a)),js_object(b)},a.env.js_create_object=function(){var a={};return js_object(a)},a.env.js_set_field_f32=function(a,b,c,d){var e=UTF8ToString(b,c);js_objects[a][e]=d},a.env.js_set_field_string=function(a,b,c,d,e){var f=UTF8ToString(b,c),g=UTF8ToString(d,e);js_objects[a][f]=g},a.env.js_unwrap_to_str=function(c,h,d){for(var e=js_objects[c],b=toUTF8Array(e),f=b.length,g=new Uint8Array(wasm_memory.buffer,h,d),a=0;a>6,128|a&63):a<55296||a>=57344?b.push(224|a>>12,128|a>>6&63,128|a&63):(c++,a=65536+((a&1023)<<10|d.charCodeAt(c)&1023),b.push(240|a>>18,128|a>>12&63,128|a>>6&63,128|a&63));return b}function js_object(b){var a=unique_js_id;return js_objects[a]=b,unique_js_id+=1,a}function consume_js_object(a){var b=js_objects[a];return delete js_objects[a],b}function get_js_object(a){return js_objects[a]}function on_init(){}register_plugin=function(a){a.env.ws_connect=ws_connect,a.env.ws_is_connected=ws_is_connected,a.env.ws_send=ws_send,a.env.ws_try_recv=ws_try_recv,a.env.http_make_request=http_make_request,a.env.http_try_recv=http_try_recv},miniquad_add_plugin({register_plugin,on_init,version:"0.1.1",name:"quad_net"});connected=0,received_buffer=[];function ws_is_connected(){return connected}function ws_connect(a){quad_socket=new WebSocket(consume_js_object(a)),quad_socket.binaryType='arraybuffer',quad_socket.onopen=function(){connected=1},quad_socket.onmessage=function(a){if(typeof a.data=="string")received_buffer.push({text:1,data:a.data});else{var b=new Uint8Array(a.data);received_buffer.push({text:0,data:b})}}}function ws_send(b){var a=consume_js_object(b);a.buffer!=void 0?quad_socket.send(a.buffer):quad_socket.send(a)}function ws_try_recv(){return received_buffer.length!=0?js_object(received_buffer.shift()):-1}uid=0,ongoing_requests={};function http_try_recv(a){if(ongoing_requests[a]!=void 0&&ongoing_requests[a]!=null){var b=ongoing_requests[a];return ongoing_requests[a]=null,js_object(b)}return-1}function http_make_request(c,f,i,j){var e=uid,b,g,h,d,a;uid+=1;c==0&&(b='POST'),c==1&&(b='PUT'),c==2&&(b='GET'),c==3&&(b='DELETE'),g=consume_js_object(f),h=consume_js_object(i),d=consume_js_object(j),a=new XMLHttpRequest,a.open(b,g,!0),a.responseType='arraybuffer';for(const b in d)a.setRequestHeader(b,d[b]);return a.onload=function(b){if(this.status==200){var a=new Uint8Array(this.response);ongoing_requests[e]=a}},a.onerror=function(a){console.error("Failed to make a request"),console.error(a)},a.send(h),e}
diff --git a/src/grammar.rs b/src/grammar.rs
new file mode 100644
index 0000000..7b09491
--- /dev/null
+++ b/src/grammar.rs
@@ -0,0 +1,75 @@
+// minimal ripoff of tracery by kate compton (tracery.io)
+use std::collections::BTreeMap;
+
+use macroquad::rand::*;
+
+pub struct Grammar {
+ pub grammar: BTreeMap>,
+}
+
+impl Grammar {
+ pub fn new() -> Self {
+ let mut grammar = BTreeMap::new();
+ grammar.insert(String::from("origin"), vec![]);
+ Self { grammar }
+ }
+
+ pub fn from(rules: impl IntoIterator- )>) -> Self {
+ let mut grammar = BTreeMap::new();
+ for (token, rule) in rules {
+ grammar.insert(token, rule);
+ }
+ Self { grammar }
+ }
+
+ pub fn insert(&mut self, key: String, val: String) {
+ let entry = self.grammar.entry(key).or_insert_with(Vec::new);
+ entry.push(val);
+ }
+
+ pub fn flatten(&self) -> Option {
+ self.flatten_with_rule(&"origin")
+ }
+
+ pub fn flatten_with_rule(&self, rule: &str) -> Option {
+ let rule = self.grammar.get(rule)?;
+ if rule.is_empty() {
+ return None;
+ }
+ let idx = gen_range(0, rule.len());
+ let rule = &rule[idx];
+ let mut output = String::from("");
+ for (i, piece) in rule.split('#').enumerate() {
+ if i % 2 == 0 {
+ output += piece;
+ } else {
+ output += &self
+ .flatten_with_rule(&piece)
+ .unwrap_or_else(|| String::from(piece));
+ }
+ }
+ Some(output)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use macroquad::rand::srand;
+
+ #[test]
+ fn test_grammar() -> Result<(), ()> {
+ use std::time::*;
+ let now = Instant::now();
+ std::thread::sleep(Duration::from_millis(150));
+ srand(now.elapsed().as_secs_f64().to_bits());
+ let mut grammar = Grammar::new();
+ grammar.insert("origin".into(), "egg".into());
+ grammar.insert("origin".into(), "sandwich".into());
+ grammar.insert("origin".into(), "#sandwich#".into());
+ grammar.insert("sandwich".into(), "i love sandwiches".into());
+ let output = grammar.flatten().ok_or(())?;
+ assert!(output == "egg" || output == "sandwich" || output == "i love sandwiches");
+ Ok(())
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 56f2e36..4da802f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,41 +1,142 @@
#![allow(clippy::upper_case_acronyms)]
#![allow(clippy::new_without_default)]
-#![allow(unused)]
use macroquad::prelude::*;
+use macroquad::rand::*;
-const FISH_PARTS_RIGHT: (&str, &str) = ("><", ">");
-const FISH_PARTS_LEFT: (&str, &str) = ("<", "><");
-const WATER_LEVEL: i32 = 100;
-const WIDTH: i32 = 780;
-const FISH_ZONE: i32 = 500;
-const FONT_SIZE: f32 = 20.0;
-const ANIM_CHANGE: u32 = 150; // in milliseconds
+pub mod grammar;
+use grammar::*;
+
+const FONT: &str = &"./assets/fira_code.ttf";
+const FONT_WIDTH: u16 = 8;
+const FONT_HEIGHT_REAL: f32 = FONT_WIDTH as f32 / 8.0 * 13.0;
+const FONT_HEIGHT: u16 = (FONT_HEIGHT_REAL + 4.0) as u16;
+const FONT_DIFFERENCE: u16 = FONT_HEIGHT - FONT_HEIGHT_REAL as u16;
+
+const GRID_WIDTH: usize = 60;
+const GRID_HEIGHT: usize = 25;
+
+// 9 = 3 + height of fishing guy - 1 for some reason
+const WATER_LEVEL: i32 = 9;
+const HEIGHT: i32 = FONT_HEIGHT as i32 * 25;
+// 90 = 60 + 30
+const WIDTH: i32 = FONT_WIDTH as i32 * 90;
+
+const UPDATE: f64 = 150.0 / 1000.0; // in seconds
+const ANIM_CHANGE: f64 = 750.0 / 1000.0;
fn window_conf() -> Conf {
Conf {
window_title: "fishing minigame".to_owned(),
window_width: WIDTH,
- window_height: FISH_ZONE,
+ window_height: HEIGHT,
fullscreen: false,
..Default::default()
}
}
struct Fish {
+ is_right: bool,
+ // middle length
length: u8,
- pos: Vec2,
+ pos: IVec2,
+}
+
+impl Fish {
+ fn new() -> Self {
+ let is_right = gen_range(0, 2) == 0;
+ let length = gen_range(0, 6);
+ Self {
+ is_right,
+ length,
+ pos: IVec2::new(
+ if is_right {
+ -(length as i32 + 3)
+ } else {
+ GRID_WIDTH as i32
+ },
+ gen_range(WATER_LEVEL + 1, GRID_HEIGHT as i32 - 2),
+ ),
+ }
+ }
+
+ fn into_text(self, grammar: &Grammar) -> Option {
+ grammar.flatten()
+ }
+}
+
+enum Mode {
+ Start,
+ Play,
+ End,
}
struct Game {
+ mode: Mode,
state: State,
draw: Draw,
+ grammar: Grammar,
}
impl Game {
- fn new() -> Self {
- Self {
- state: State::new(),
- draw: Draw::new(),
+ async fn new() -> Result {
+ let mut draw = Draw::new().await.unwrap();
+ let mut state = State::new();
+ game_setup(&mut state, &mut draw);
+ let grammar = grammar_setup();
+
+ Ok(Self {
+ mode: Mode::Start,
+ state,
+ draw,
+ grammar,
+ })
+ }
+
+ fn update(&mut self) {
+ // fish update
+ let mut remove = Vec::new();
+ for (i, fish) in self.state.fish.iter_mut().enumerate() {
+ // if something is hooked and it's not this
+ let is_hooked = self.state.is_hooked.map(|idx| idx == i).unwrap_or(false);
+ if !is_hooked {
+ if fish.is_right {
+ fish.pos.x += 1;
+ if fish.pos.x >= GRID_WIDTH as i32 {
+ remove.push(i);
+ }
+ } else {
+ fish.pos.x -= 1;
+ if fish.pos.x <= -(fish.length as i32 + 3) {
+ remove.push(i);
+ }
+ }
+ }
+ }
+ for i in remove.into_iter().rev() {
+ self.state.fish.remove(i);
+ if let Some(idx) = &mut self.state.is_hooked {
+ if *idx > i {
+ *idx -= 1;
+ }
+ }
+ }
+
+ if self.state.fish.len() < 3 && gen_range(0, 15) < 1 {
+ self.state.fish.push(Fish::new());
+ }
+
+ // collision
+ let hook = self.state.hook;
+ if self.state.is_hooked.is_none() {
+ for (i, fish) in self.state.fish.iter().enumerate() {
+ if fish.pos.y == hook.y
+ && fish.pos.x <= hook.x
+ && fish.pos.x + fish.length as i32 + 3 > hook.x
+ {
+ self.state.is_hooked = Some(i);
+ break;
+ }
+ }
}
}
}
@@ -43,16 +144,78 @@ impl Game {
struct Draw {
// only two frames of animation
first_frame: bool,
- frame_counter: usize,
+ // animated sprites
animations: Vec,
+ // static sprites
+ sprites: Vec,
+ font: Font,
}
impl Draw {
- fn new() -> Self {
- Self {
+ async fn new() -> Result {
+ // font: fira code. dimensions: 13.0x8.0
+ let font = load_ttf_font(FONT).await?;
+ Ok(Self {
first_frame: true,
- frame_counter: 0,
animations: Vec::new(),
+ sprites: Vec::new(),
+ font,
+ })
+ }
+
+ fn add_sprite(&mut self, sprite: Sprite) -> usize {
+ self.sprites.push(sprite);
+ self.sprites.len() - 1
+ }
+
+ fn add_animation(&mut self, animation: Animation) -> usize {
+ self.animations.push(animation);
+ self.animations.len() - 1
+ }
+}
+
+struct Sprite {
+ width: usize,
+ height: usize,
+ color: Option,
+ sprite: String,
+}
+
+impl Sprite {
+ fn new(width: usize, height: usize, color: Option, sprite: String) -> Self {
+ assert_eq!(width * height, sprite.len() - height + 1);
+ Self {
+ width,
+ height,
+ color,
+ sprite,
+ }
+ }
+
+ fn draw(&self, pos: IVec2, font: Font) {
+ let pos = pos.as_f32() * Vec2::new(FONT_WIDTH as f32, FONT_HEIGHT as f32);
+ if let Some(color) = self.color {
+ draw_rectangle(
+ pos.x,
+ pos.y,
+ self.width as f32 * FONT_WIDTH as f32,
+ self.height as f32 * FONT_HEIGHT as f32,
+ color,
+ );
+ }
+ for (i, row) in self.sprite.split('\n').enumerate() {
+ let i = (i + 1) as f32 * FONT_HEIGHT as f32 - FONT_DIFFERENCE as f32;
+ draw_text_ex(
+ row,
+ pos.x,
+ pos.y + i,
+ TextParams {
+ font,
+ color: BLACK,
+ font_size: FONT_HEIGHT - FONT_DIFFERENCE,
+ ..Default::default()
+ },
+ );
}
}
}
@@ -61,13 +224,14 @@ struct Animation {
width: usize,
height: usize,
color: Color,
+ /// animations can have two frames and must be strings
frames: [String; 2],
}
impl Animation {
fn new(width: usize, height: usize, color: Color, frames: [String; 2]) -> Self {
- assert_eq!(width * height, frames[0].len());
- assert_eq!(width * height, frames[1].len());
+ assert_eq!(width * height, frames[0].len() - height + 1);
+ assert_eq!(width * height, frames[1].len() - height + 1);
Self {
width,
height,
@@ -76,28 +240,58 @@ impl Animation {
}
}
- fn draw(&self, pos: Vec2) {
+ fn draw(&self, pos: IVec2, first_frame: bool, font: Font) {
+ let pos = pos.as_f32() * Vec2::new(FONT_WIDTH as f32, FONT_HEIGHT as f32);
draw_rectangle(
pos.x,
pos.y,
- self.width as f32 * FONT_SIZE,
- self.height as f32 * FONT_SIZE,
+ self.width as f32 * FONT_WIDTH as f32,
+ self.height as f32 * FONT_HEIGHT as f32,
self.color,
);
+ for (i, row) in self.frames[first_frame as usize].split('\n').enumerate() {
+ let i = (i + 1) as f32 * FONT_HEIGHT as f32 - FONT_DIFFERENCE as f32;
+ draw_text_ex(
+ row,
+ pos.x,
+ pos.y + i,
+ TextParams {
+ font,
+ color: BLACK,
+ font_size: FONT_HEIGHT - FONT_DIFFERENCE,
+ ..Default::default()
+ },
+ );
+ }
}
}
struct State {
+ // TODO: consider vecdeque instead of stack
fish: Vec,
- player: Vec2,
+ hook: IVec2,
+ // which fish is hooked
+ is_hooked: Option,
+ // positions of other things (unmoving)
+ positions: Vec,
+ // parallel vec of sprites corresponding to ^
+ sprites: Vec,
caught_fish: Vec,
}
+enum DrawType {
+ Anim(usize),
+ Sprite(usize),
+}
+
impl State {
fn new() -> Self {
State {
fish: Vec::new(),
- player: Vec2::new(FISH_ZONE as f32 / 2.0, WATER_LEVEL as f32),
+ hook: IVec2::new(GRID_WIDTH as i32 / 2, WATER_LEVEL + 5),
+ is_hooked: None,
+ positions: Vec::new(),
+ sprites: Vec::new(),
caught_fish: Vec::new(),
}
}
@@ -105,27 +299,418 @@ impl State {
#[macroquad::main(window_conf)]
async fn main() {
- let mut game = Game::new();
+ srand(get_time().to_bits());
+ let mut game = Game::new().await.unwrap();
+
+ let mut updated = get_time();
+ let mut anim_updated = get_time();
+
loop {
- if is_key_down(KeyCode::Escape) {
- break;
+ match game.mode {
+ Mode::Start => {
+ // draw
+ clear_background(SKYBLUE);
+ draw_text_centered(
+ &"press ENTER to start",
+ HEIGHT as f32 / 2.0 - FONT_HEIGHT as f32 * 2.0,
+ game.draw.font,
+ );
+ draw_text_centered(
+ &"use up and down to move the hook",
+ HEIGHT as f32 / 2.0 - FONT_HEIGHT as f32,
+ game.draw.font,
+ );
+ draw_text_centered(
+ &"use space to catch a fish",
+ HEIGHT as f32 / 2.0,
+ game.draw.font,
+ );
+ draw_text_centered(
+ &"use escape to end the game",
+ HEIGHT as f32 / 2.0 + FONT_HEIGHT as f32,
+ game.draw.font,
+ );
+ if is_key_pressed(KeyCode::Enter) {
+ game.mode = Mode::Play;
+ }
+ }
+ Mode::Play => {
+ if is_key_pressed(KeyCode::Escape) {
+ game.mode = Mode::End;
+ continue;
+ }
+ if is_key_pressed(KeyCode::Up) {
+ game.state.hook.y = (game.state.hook.y - 1).max(WATER_LEVEL);
+ }
+ if is_key_pressed(KeyCode::Down) {
+ game.state.hook.y = (game.state.hook.y + 1).min(GRID_HEIGHT as i32 - 2);
+ }
+
+ if let Some(idx) = game.state.is_hooked {
+ game.state.fish[idx].pos.y = game.state.hook.y;
+
+ if is_key_pressed(KeyCode::Space) {
+ let fish = game.state.fish.remove(idx);
+ game.state
+ .caught_fish
+ .push(fish.into_text(&game.grammar).unwrap());
+ game.state.is_hooked = None;
+ }
+ }
+
+ if get_time() - updated >= UPDATE {
+ game.update();
+ updated = get_time();
+ }
+ game.draw();
+ if get_time() - anim_updated >= ANIM_CHANGE {
+ game.draw.first_frame = !game.draw.first_frame;
+ anim_updated = get_time();
+ }
+ }
+ Mode::End => {
+ clear_background(SKYBLUE);
+ draw_text_centered(
+ &format!(
+ "you caught {} fish, thanks for playing",
+ game.state.caught_fish.len()
+ ),
+ HEIGHT as f32 / 2.0,
+ game.draw.font,
+ );
+ draw_text_centered(
+ &"press ENTER to restart",
+ HEIGHT as f32 / 2.0 + FONT_HEIGHT as f32,
+ game.draw.font,
+ );
+ if is_key_pressed(KeyCode::Enter) {
+ game.state = State::new();
+ game.draw.animations.clear();
+ game.draw.sprites.clear();
+ game_setup(&mut game.state, &mut game.draw);
+ game.mode = Mode::Start;
+ }
+ }
}
- draw(&game);
next_frame().await;
}
}
-fn draw(game: &Game) {
- const FZ: f32 = FISH_ZONE as f32;
- const WL: f32 = WATER_LEVEL as f32;
- clear_background(SKYBLUE);
- // sky
- draw_rectangle(0.0, 0.0, FZ, WL, WHITE);
- // ground
- draw_rectangle(0.0, FZ - 30.0, FZ, 30.0, BEIGE);
- // static sprites
- // animated stuff
+impl Game {
+ fn draw(&self) {
+ const FISH_WIDTH: f32 = FONT_WIDTH as f32 * GRID_WIDTH as f32;
+ const H: f32 = HEIGHT as f32;
+ const WL: f32 = WATER_LEVEL as f32 * FONT_HEIGHT as f32;
+ // water
+ clear_background(SKYBLUE);
+ // sky
+ draw_rectangle(0.0, 0.0, FISH_WIDTH, WL, WHITE);
+ // ground
+ draw_rectangle(
+ 0.0,
+ H - FONT_HEIGHT as f32,
+ FISH_WIDTH,
+ FONT_HEIGHT as f32,
+ BEIGE,
+ );
+
+ // static sprites
+ for (pos, sprite) in self.state.positions.iter().zip(self.state.sprites.iter()) {
+ match sprite {
+ DrawType::Anim(i) => {
+ self.draw.animations[*i].draw(*pos, self.draw.first_frame, self.draw.font)
+ }
+ DrawType::Sprite(i) => self.draw.sprites[*i].draw(*pos, self.draw.font),
+ }
+ }
+
+ // fish
+ for fish in self.state.fish.iter() {
+ let sprite = &self.draw.sprites[fish.length as usize * 2 + fish.is_right as usize];
+ sprite.draw(fish.pos, self.draw.font);
+ }
+ // hook
+ self.draw.sprites[12].draw(self.state.hook, self.draw.font);
+
+ // line
+ let sprite = (0..self.state.hook.y - 5)
+ .map(|_| "|\n")
+ .collect::();
+ let sprite = sprite.trim().to_string();
+ let sprite = Sprite::new(1, self.state.hook.y as usize - 5, None, sprite);
+ sprite.draw(IVec2::new(GRID_WIDTH as i32 / 2, 5), self.draw.font);
+
+ // sidebar
+ draw_rectangle(FISH_WIDTH, 0.0, WIDTH as f32 - FISH_WIDTH, H, LIGHTGRAY);
+
+ // draw fish info
+ // sidebar width: 480 - 8px? margins * 2 = 464 / 16 = 29 characters / line
+ let mut y = 30.0;
+ let x = GRID_WIDTH as f32 * FONT_WIDTH as f32 + 8.0;
+
+ for text in self.state.caught_fish.iter().rev() {
+ if y > HEIGHT as f32 + FONT_HEIGHT as f32 {
+ break;
+ }
+ y += 20.0;
+ let mut line = "".to_string();
+ line.reserve_exact(28);
+ for word in text.split_whitespace() {
+ if line.len() + word.len() > 28 {
+ draw_text_ex(
+ &line,
+ x,
+ y,
+ TextParams {
+ font: self.draw.font,
+ color: BLACK,
+ font_size: FONT_HEIGHT - FONT_DIFFERENCE,
+ ..Default::default()
+ },
+ );
+ line.clear();
+ y += FONT_HEIGHT as f32;
+ }
+ line += word;
+ line += " ";
+ }
+
+ draw_text_ex(
+ &line,
+ x,
+ y,
+ TextParams {
+ font: self.draw.font,
+ color: BLACK,
+ font_size: FONT_HEIGHT - FONT_DIFFERENCE,
+ ..Default::default()
+ },
+ );
+ y += FONT_HEIGHT as f32;
+ }
+ }
+}
+
+fn grammar_setup() -> Grammar {
+ let rules = [
+ ("origin".to_string(),
+ vec!["#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#fish#. #fishFact#".to_string(),
+ "#seaFish#. #seaFishFact#".to_string(),
+ "#largeFish#. #largeFishFact#".to_string(),
+ "#whale#. #whaleFact#".to_string(),
+ "#unnervingFish#. #unnervingFishFact#".to_string(),
+ "sea bass. no, wait--- it's at least a c+!".to_string()]),
+
+ ("fish".to_string(),
+ vec!["rainbow trout".to_string(),
+ "salmon".to_string(),
+ "black bass".to_string(),
+ "catfish".to_string(),
+ "brook trout".to_string(),
+ "cod".to_string(),
+ "minnow".to_string(),
+ "turbofish".to_string()]),
+
+ ("fishFact".to_string(),
+ vec!["this fish loves to swim about.".to_string(),
+ "this fish was sleeping and you woke it up, which is kind of rude.".to_string(),
+ "this fish will grant you three wishes if you don't eat it but you weren't going to do that anyway.".to_string(),
+ "fish are neat :)".to_string(),
+ "this fish likes to hide under rocks.".to_string(),
+ "this fish can breathe underwater.".to_string(),
+ "this fish likes to blow bubbles.".to_string(),
+ "this fish likes to flick its tail.".to_string(),
+ "this fish can do a backflip.".to_string(),
+ "this fish is very tired and wants to rest.".to_string(),
+ "this fish wants to show you some sick card tricks.".to_string()]),
+
+ ("unnervingFish".to_string(),
+ vec!["swamp eel".to_string(),
+ "pike".to_string(),
+ "jellyfish".to_string(),
+ "sunfish".to_string(),
+ "angler fish".to_string(),
+ "squid".to_string()]),
+
+ ("unnervingFishFact".to_string(),
+ vec!["#fishFact#".to_string(),
+ "i'm spooked by this fish.".to_string(),
+ "this fish has so many teeth.".to_string(),
+ "you feel like this fish will haunt your nightmares.".to_string(),
+ "please put it back.".to_string()]),
+
+ ("largeFish".to_string(),
+ vec!["lemon shark".to_string(),
+ "mako shark".to_string(),
+ "whale shark".to_string(),
+ "great white shark".to_string()]),
+
+ ("largeFishFact".to_string(),
+ vec!["#seaFishFact#".to_string(),
+ "#fishFact#".to_string(),
+ "this fish is incredibly large.".to_string(),
+ "OH GOD IT'S JUST SO BIG".to_string(),
+ "this fish might eat you? you're not sure, it hasn't happened yet".to_string(),
+ "this fish has so many teeth.".to_string()]),
+
+ ("whale".to_string(),
+ vec!["blue whale".to_string(),
+ "humpback whale".to_string(),
+ "sperm whale".to_string(),
+ "right whale".to_string(),
+ "razor back whale".to_string()]),
+
+ ("whaleFact".to_string(),
+ vec!["#seaFishFact#".to_string(),
+ "#largeFishFact#".to_string(),
+ "\"Oh, man! Admire and model thyself after the whale! Do thou, too, remain warm among ice. Do thou, too, live in this world without being of it. Be cool at the equator; keep thy blood fluid at the pole. Like the great dome of St. Peter's, and like the great whale, retain, O man! in all seasons a temperature of thine own.\" (melville 343)".to_string(),
+ "\"I. A Fast-Fish belongs to the party fast to it. II. A Loose-Fish is fair game for anybody who can soonest catch it.\" (melville 441)".to_string(),
+ "\"All men live enveloped in whale lines.\" (melville 315)".to_string(),
+ "\"Let him go. I know little more of him,
+ nor does anybody else.\" (melville 153)".to_string(),
+ "\"a whale is A SPOUTING FISH WITH A HORIZONTAL TAIL. There you have him.\" (melville 148)".to_string()]),
+
+ ("seaFish".to_string(),
+ vec!["black sea bass".to_string(),
+ "mackerel".to_string(),
+ "tuna".to_string(),
+ "sea bass".to_string(),
+ "anchovy".to_string()]),
+
+ ("seaFishFact".to_string(),
+ vec!["#fishFact#".to_string(),
+ "this fish lives in the sea and you're not sure how it got here.".to_string(),
+ "you're not even sure if this fish can live in freshwater, but okay.".to_string()])
+ ];
+ Grammar::from(rules)
+}
+
+fn game_setup(state: &mut State, draw: &mut Draw) {
// fish
- // sidebar
- draw_rectangle(FZ, 0.0, WIDTH as f32 - FZ, FZ, LIGHTGRAY);
+ for i in 0..6 {
+ let mut middle = "".to_string();
+ middle.reserve_exact(i);
+ for _ in 0..i {
+ middle += ":";
+ }
+ draw.add_sprite(Sprite::new(
+ i + 3,
+ 1,
+ Some(SKYBLUE),
+ "<".to_owned() + &middle.clone() + "><",
+ ));
+ draw.add_sprite(Sprite::new(
+ i + 3,
+ 1,
+ Some(SKYBLUE),
+ "><".to_owned() + &middle + ">",
+ ));
+ }
+
+ // hook
+ draw.add_sprite(Sprite::new(1, 1, Some(SKYBLUE), "&".to_owned()));
+
+ // seaweed
+ let mut plant_already = [false; GRID_WIDTH];
+ for _ in 0..5 {
+ let mut x = gen_range(1, GRID_WIDTH - 1);
+ while plant_already[x] {
+ x = gen_range(1, GRID_WIDTH - 1);
+ }
+ plant_already[x] = true;
+ let height = gen_range(2, (GRID_HEIGHT - WATER_LEVEL as usize) / 2);
+ let mut plant = "".to_string();
+ plant.reserve_exact(height + 2);
+ for _ in 0..height / 2 + 1 {
+ plant += "\\\n/\n";
+ }
+ let start = gen_range(0, 2) == 0;
+
+ let anim = if start {
+ [
+ plant[..height * 2 - 1].to_string(),
+ plant[2..height * 2 + 1].to_string(),
+ ]
+ } else {
+ [
+ plant[2..height * 2 + 1].to_string(),
+ plant[..height * 2 - 1].to_string(),
+ ]
+ };
+ let anim = Animation::new(1, height, GREEN, anim);
+ let i = draw.add_animation(anim);
+ state
+ .positions
+ .push(IVec2::new(x as i32, GRID_HEIGHT as i32 - height as i32 - 1));
+ state.sprites.push(DrawType::Anim(i));
+ }
+
+ // water
+ let mut water = "^".to_string();
+ water.reserve_exact(GRID_WIDTH);
+ for _ in 0..GRID_WIDTH / 2 {
+ water += "~^";
+ }
+ let anim = Animation::new(
+ GRID_WIDTH,
+ 1,
+ SKYBLUE,
+ [water[..GRID_WIDTH].to_string(), water[1..].to_string()],
+ );
+ let i = draw.add_animation(anim);
+ state.positions.push(IVec2::new(0, WATER_LEVEL));
+ state.sprites.push(DrawType::Anim(i));
+
+ // ground
+ let groud_chars = [
+ "-", "=", ".", "__", "--", "==", "..", "___", "---", "===", "...",
+ ];
+ let mut ground = "".to_string();
+ let mut i = 0;
+ while i < GRID_WIDTH {
+ let add = groud_chars[gen_range(0, groud_chars.len())];
+ i += add.len();
+ ground += add;
+ }
+
+ ground = ground[..GRID_WIDTH].to_string();
+ let i = draw.add_sprite(Sprite::new(GRID_WIDTH, 1, Some(BEIGE), ground));
+ state.positions.push(IVec2::new(0, GRID_HEIGHT as i32 - 1));
+ state.sprites.push(DrawType::Sprite(i));
+
+ // water
+ // dude
+ #[rustfmt::skip]
+ let dude = r#" O
+ |--o_______________________.
+ |__
+===== |
+ | | |
+ | | "#;
+ let i = draw.add_sprite(Sprite::new(31, 6, None, dude.to_string()));
+ state.positions.push(IVec2::new(0, 3));
+ state.sprites.push(DrawType::Sprite(i));
+}
+
+fn draw_text_centered(text: &str, y: f32, font: Font) {
+ let dims = measure_text(text, Some(font), FONT_HEIGHT - FONT_DIFFERENCE, 1.0);
+ draw_text_ex(
+ text,
+ WIDTH as f32 / 2.0 - dims.width / 2.0,
+ y,
+ TextParams {
+ font,
+ color: BLACK,
+ font_size: FONT_HEIGHT - FONT_DIFFERENCE,
+ ..Default::default()
+ },
+ );
}