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() + }, + ); }