diff --git a/contracts/AnybodyProblem.sol b/contracts/AnybodyProblem.sol index 8f6b4251..2ce8cf9f 100644 --- a/contracts/AnybodyProblem.sol +++ b/contracts/AnybodyProblem.sol @@ -21,7 +21,9 @@ contract AnybodyProblem is Ownable, ERC2981 { uint256 public constant FIRST_SUNDAY_AT_6_PM_UTC = 324000; bool public paused = false; - uint256 public price = 0.0025 ether; + uint256 public priceToMint = 0.0025 ether; + uint256 public priceToSave = 0 ether; + uint256 public discount = 2; address payable public proceedRecipient; address public externalMetadata; address payable public speedruns; @@ -94,9 +96,9 @@ contract AnybodyProblem is Ownable, ERC2981 { uint256[] memory verifiersTicks, uint256[] memory verifiersBodies ) { - updateExternalMetadata(externalMetadata_); updateProceedRecipient(proceedRecipient_); updateSpeedrunsAddress(speedruns_); + updateExternalMetadata(externalMetadata_); for (uint256 i = 0; i < verifiers_.length; i++) { require(verifiersTicks[i] > 0, 'Invalid verifier'); require(verifiers_[i] != address(0), 'Invalid verifier'); @@ -137,6 +139,8 @@ contract AnybodyProblem is Ownable, ERC2981 { // NOTE: the only publicly available function that isn't protected by a modifier function batchSolve( uint256 runId, + bool alsoMint, + uint256 day, uint256[] memory tickCounts, uint[2][] memory a, uint[2][2][] memory b, @@ -144,7 +148,7 @@ contract AnybodyProblem is Ownable, ERC2981 { uint[][] memory input ) public payable { require(!paused, 'Contract is paused'); - uint256 day = currentDay(); + // uint256 day = currentDay(); // TODO: this version does not enforce day for solving, just for minting NFT if (runId == 0) { runId = addNewRun(day); addNewLevelData(runId); @@ -163,6 +167,7 @@ contract AnybodyProblem is Ownable, ERC2981 { for (uint256 i = 0; i < input.length; i++) { verifyLevelChunk( runId, + alsoMint, tickCounts[i], day, a[i], @@ -247,6 +252,7 @@ contract AnybodyProblem is Ownable, ERC2981 { } function genRadius(uint256 index) public pure returns (uint256) { + // uint8[6] memory radii = [36, 27, 23, 19, 15, 11]; // n * 4 + 2 TODO: swtich to x4 on next deployment uint8[6] memory radii = [36, 27, 22, 17, 12, 7]; // n * 5 + 2 return radii[index % radii.length] * scalingFactor; } @@ -309,6 +315,7 @@ contract AnybodyProblem is Ownable, ERC2981 { function verifyLevelChunk( uint256 runId, + bool alsoMint, uint256 tickCount, uint256 day, uint[2] memory a, @@ -414,12 +421,11 @@ contract AnybodyProblem is Ownable, ERC2981 { runs[runId].accumulativeTime += levelData.time; if (level == LEVELS) { runs[runId].solved = true; - require(msg.value == price, 'Incorrect payment'); - (bool sent, bytes memory data) = proceedRecipient.call{ - value: msg.value - }(''); - Speedruns(speedruns).__mint(msg.sender, day, 1, ''); - emit EthMoved(proceedRecipient, sent, data, msg.value); + if (alsoMint) { + mint(priceToSave + (priceToMint / discount), day); + } else if (priceToSave > 0) { + makePayment(priceToSave); + } emit RunSolved( msg.sender, runId, @@ -434,6 +440,26 @@ contract AnybodyProblem is Ownable, ERC2981 { } } + function makePayment(uint256 payment) internal { + require(msg.value >= payment, 'Incorrect payment'); + require(proceedRecipient != address(0), 'Invalid recipient'); + (bool sent, bytes memory data) = proceedRecipient.call{value: payment}( + '' + ); + emit EthMoved(proceedRecipient, sent, data, payment); + } + + function mint(uint256 payment, uint256 day) internal { + require(day == currentDay(), 'Can only mint on the current day'); + require(payment == priceToMint, 'Incorrect price'); + makePayment(payment); + Speedruns(speedruns).__mint(msg.sender, day, 1, ''); + } + + function mint() public payable { + mint(msg.value, currentDay()); + } + function addToLeaderboard(uint256 runId) internal { addToFastestByDay(runId); addToLongestStreak(runId); @@ -839,8 +865,16 @@ contract AnybodyProblem is Ownable, ERC2981 { emit EthMoved(_to, sent, data, amount); } - function updatePrice(uint256 price_) public onlyOwner { - price = price_; + function updateDiscount(uint256 discount_) public onlyOwner { + discount = discount_; + } + + function updatePriceToSave(uint256 priceToSave_) public onlyOwner { + priceToSave = priceToSave_; + } + + function updatePriceToMint(uint256 priceToMint_) public onlyOwner { + priceToMint = priceToMint_; } function updatePaused(bool paused_) public onlyOwner { diff --git a/contracts/ExternalMetadata.sol b/contracts/ExternalMetadata.sol index ceb0afc0..3055ea24 100644 --- a/contracts/ExternalMetadata.sol +++ b/contracts/ExternalMetadata.sol @@ -382,6 +382,7 @@ contract ExternalMetadata is Ownable { '"external_url": "https://anybody.trifle.life",', '"animation_url": "https://anybody-nft.netlify.app/#', StringsExtended.toString(date), + getBestTimeEncoded(date), '",', '"attributes": ', getAttributes(date), @@ -392,6 +393,35 @@ contract ExternalMetadata is Ownable { ); } + function getBestTimeEncoded( + uint256 date + ) public view returns (string memory) { + uint256 bestRunId = AnybodyProblem(anybodyProblem).fastestByDay( + date, + 0 + ); + + AnybodyProblem.Level[] memory levels = AnybodyProblem(anybodyProblem) + .getLevelsData(bestRunId); + + string memory encoded = '{"levels":['; + + for (uint256 i = 0; i < levels.length; i++) { + AnybodyProblem.Level memory level = levels[i]; + encoded = string( + abi.encodePacked( + encoded, + '{"events": [{"time":', + StringsExtended.toString(level.time), + '}]}', + (i == levels.length - 1 ? '' : ',') + ) + ); + } + encoded = Base64.encode(abi.encodePacked(encoded, ']}')); + return string(abi.encodePacked('-', encoded)); + } + function getName(uint256 date) public pure returns (string memory) { (uint year, uint month, uint day) = BokkyPooBahsDateTimeLibrary .timestampToDate(date); @@ -692,62 +722,60 @@ contract ExternalMetadata is Ownable { return path; } + function getFastestTime( + uint256 date, + uint256 placeIndex + ) public view returns (address, string memory sec) { + uint256 runId = AnybodyProblem(anybodyProblem).fastestByDay( + date, + placeIndex + ); + (address player, , uint256 timeCompleted, , ) = AnybodyProblem( + anybodyProblem + ).runs(runId); + + uint256 precision = 1000; + uint256 fps = 25; + timeCompleted = (timeCompleted * precision) / fps; + uint256 timeSeconds = timeCompleted / precision; + uint256 timeMs = (timeCompleted - timeSeconds * precision) / 10; + + return ( + player, + string( + abi.encodePacked( + StringsExtended.toString(timeSeconds), + '.', + StringsExtended.toString(timeMs) + ) + ) + ); + } + /// @dev generates the attributes as JSON String function getAttributes(uint256 date) public view returns (string memory) { (uint year, uint month, ) = BokkyPooBahsDateTimeLibrary.timestampToDate( date ); - uint256 fastestRunId = AnybodyProblem(anybodyProblem).fastestByDay( + + (address fastestAddress, string memory fastestTime) = getFastestTime( date, 0 ); - uint256 secondFastestRunId = AnybodyProblem(anybodyProblem) - .fastestByDay(date, 1); - uint256 thirdFastestRunId = AnybodyProblem(anybodyProblem).fastestByDay( - date, - 2 - ); - (address fastestAddress, , uint256 fastestTime, , ) = AnybodyProblem( - anybodyProblem - ).runs(fastestRunId); ( address secondFastestAddress, - , - uint256 secondFastestTime, - , - - ) = AnybodyProblem(anybodyProblem).runs(secondFastestRunId); + string memory secondFastestTime + ) = getFastestTime(date, 1); ( address thirdFastestAddress, - , - uint256 thirdFastestTime, - , - - ) = AnybodyProblem(anybodyProblem).runs(thirdFastestRunId); - - uint256 precision = 1000; - uint256 fps = 25; - fastestTime = (fastestTime * precision) / fps; - uint256 fastestTimeSeconds = fastestTime / precision; - uint256 fastestTimeMs = (fastestTime - fastestTimeSeconds * precision) / - 10; - - secondFastestTime = (secondFastestTime * precision) / fps; - uint256 secondFastestTimeSeconds = secondFastestTime / precision; - uint256 secondFastestTimeMs = (secondFastestTime - - secondFastestTimeSeconds * - precision) / 10; - - thirdFastestTime = (thirdFastestTime * precision) / fps; - uint256 thirdFastestTimeSeconds = thirdFastestTime / precision; - uint256 thirdFastestTimeMs = (thirdFastestTime - - thirdFastestTimeSeconds * - precision) / 10; + string memory thirdFastestTime + ) = getFastestTime(date, 2); return string( abi.encodePacked( '[', + '{"trait_type":"Galaxy","value":"BASED"},', '{"trait_type":"Day","value":"', StringsExtended.toString(date), '"}, {"trait_type":"Year-Month","value":"', @@ -758,21 +786,15 @@ contract ExternalMetadata is Ownable { '"}, {"trait_type":"1st Place","value":"0x', StringsExtended.toHexString(fastestAddress), '"}, {"trait_type":"1st Place Time (s)","value":"', - StringsExtended.toString(fastestTimeSeconds), - '.', - StringsExtended.toString(fastestTimeMs), + fastestTime, '"}, {"trait_type":"2nd Place","value":"0x', StringsExtended.toHexString(secondFastestAddress), '"}, {"trait_type":"2nd Place Time (s)","value":"', - StringsExtended.toString(secondFastestTimeSeconds), - '.', - StringsExtended.toString(secondFastestTimeMs), + secondFastestTime, '"}, {"trait_type":"3rd Place","value":"0x', StringsExtended.toHexString(thirdFastestAddress), '"}, {"trait_type":"3rd Place Time (s)","value":"', - StringsExtended.toString(thirdFastestTimeSeconds), - '.', - StringsExtended.toString(thirdFastestTimeMs), + thirdFastestTime, '"}]' ) ); diff --git a/dist/index.html b/dist/index.html index bbaf90fd..9600c0c8 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,6 +1,6 @@ -
\ No newline at end of file + `,document.head.appendChild(F)}clearValues(){this.level<=1&&(this.levelSpeeds=[,,,,,]),this.skip0&&0==this.level&&(this.level=1),this.lastMissileCantBeUndone=!1,this.speedFactor=2,this.speedLimit=10,this.missileSpeed=15,this.shownStatScreen=!1,this.G=100,this.vectorLimit=this.speedLimit*this.speedFactor,this.missileVectorLimit=this.missileSpeed*this.speedFactor,this.FPS=25,this.P5_FPS_MULTIPLIER=3,this.P5_FPS=this.FPS*this.P5_FPS_MULTIPLIER,this.p?.frameRate(this.P5_FPS),this.timer=(this.level>5?60:In[this.level])*this.FPS,this.deadOpacity="0.9",this.initialScoreSize=120,this.scoreSize=this.initialScoreSize,this.opac=(this.globalStyle,1),this.tailLength=1,this.tailMod="psycho"==this.globalStyle?2:1,this.explosions=[],this.missiles=[],this.stillVisibleMissiles=[],this.missileInits=[],this.bodies=[],this.witheringBodies=[],this.bodyInits=[],this.bodyFinal=[],this.missileCount=0,this.frames=0,this.p5Frames=0,this.showIt=!0,this.justStopped=!1,this.gameOver=!1,this.firstFrame=!0,this.loaded=!1,this.showPlayAgain=!1,this.handledGameOver=!1,this.statsText="",this.hasStarted=!1,this.buttons={},this.won=!1,this.finalBatchSent=!1,this.solved=!1,this.shaking=0,this.explosionSmoke=[],this.gunSmoke=[],this.date=new Date(1e3*this.day).toISOString().split("T")[0].replace(/-/g,"."),this.framesTook=!1,this.showProblemRankingsScreenAt=-1,this.saveStatus="unsaved",delete this.validatedAt,delete this.validatingAt,delete this.savingAt,delete this.savedAt}init(){this.skipAhead=!1,this.winScreenBaddies=void 0,this.seed=q7.solidityKeccak256(["uint256"],[this.day]),this.generateBodies(),this.frames=this.alreadyRun,this.startingFrame=this.alreadyRun,this.stopEvery=this.test?20:this.proverTickIndex(this.level+1),this.lastLevel=this.level,this.setPause(this.paused,!0),this.storeInits()}async start(){this.addCSS(),this.addListeners(),this.runSteps(this.preRun),this.freeze&&this.setPause(!0,!0)}destroy(){this.setPause(!0),this.p.noLoop(),this.removeListener(),this.sound.stop(),this.sound=null,this.p.remove()}storeInits(){this.bodyInits=this.processInits(this.bodies)}processInits(F){return this.convertBodiesToBigInts(F).map(F=>((F=this.convertScaledBigIntBodyToArray(F))[2]=BigInt(F[2]).toString(),F[3]=BigInt(F[3]).toString(),F))}runSteps(F=this.preRun){let A=0,t=!0;for(this.showIt=!1;t;)if(++A>F)t=!1,this.showIt=!0;else{let F=this.step(this.bodies,this.missiles);this.frames++,this.bodies=F.bodies,this.missiles=F.missiles||[]}}addListeners(){this.p.mouseMoved=this.handleMouseMove,this.p.touchStarted=F=>{this.hasTouched=!0,this.handleGameClick(F)},this.p.mouseClicked=this.handleGameClick,this.p.keyPressed=this.handleGameKeyDown}removeListener(){this.p.remove()}getXY(F){let A=F.offsetX||F.layerX,t=F.offsetY||F.layerY,e=F.target.getBoundingClientRect(),i=e.width,B=e.height;return{x:A=A*this.windowWidth/i,y:t=t*this.windowHeight/B}}handleMouseMove=F=>{let{x:A,y:t}=this.getXY(F);for(let F in this.mouseX=A,this.mouseY=t,this.buttons){let e=this.buttons[F];e.hover=Io(e,A,t)}};handleGameClick=F=>{this.gameOver&&this.won&&(this.skipAhead=!0);let{x:A,y:t}=this.getXY(F);for(let F in this.buttons){let e=this.buttons[F];if(e.visible&&Io(e,A,t)&&!e.disabled){e.onClick();return}}this.paused||this.gameOver||this.missileClick(A,t)};handleNFTClick=()=>{this.setPause()};handleGameKeyDown=F=>{if(this.gameOver&&this.won&&(this.skipAhead=!0),!(F.shiftKey&&F.altKey&&F.ctrlKey&&F.metaKey))switch(F.code){case"Space":(this.mouseX||this.mouseY)&&(F.preventDefault(),this.missileClick(this.mouseX,this.mouseY));break;case"KeyR":this.restart(null,!1);break;case"KeyP":this.gameOver||this.setPause()}};handleGameOver=({won:F})=>{if(this.handledGameOver)return;if(this.handledGameOver=!0,this.gameoverTickerX=0,this.gameOver=!0,this.won=F,0!==this.level&&!this.won){let F=this.bodies.slice(1).filter(F=>0n!==F.radius).length,A=this.generateLevelData(this.day,6-F).slice(1);this.bodies.push(...A.map(F=>this.bodyDataToBodies.call(this,F)).map(F=>(F.position.x=0,F.position.y=0,F.py=0n,F.px=0n,F)))}this.P5_FPS*=2,this.p.frameRate(this.P5_FPS);var A=0;let t=this.calculateStats();A=t.timeTook,this.framesTook=t.framesTook,this.emit("done",{level:this.level,won:F,ticks:this.frames-this.startingFrame,timeTook:A,framesTook:this.framesTook}),F&&(this.bodyData=null,this.finish())};restart=(F,A=!0)=>{F&&this.setOptions(F),this.level,this.lastLevel,this.clearValues(),this.sound?.stop(),this.sound?.playStart(),this.sound?.setSong(),this.init(),this.draw(),A&&this.setPause(!0),this.addCSS()};doubleTextInverted(F){return F.slice(0,-1)+F.split("").reverse().join("")}setPause(F=!this.paused,A=!1){"boolean"!=typeof F&&(F=!this.paused),F?(this.pauseBodies=Ia.map(F=>this.bodyDataToBodies.call(this,F)),this.pauseBodies[1].c=this.getBodyColor(this.day+1,0),this.pauseBodies[2].c=this.getBodyColor(this.day+2,0),this.paused=F,this.willUnpause=!1,delete this.beganUnpauseAt):(this.justPaused=!0,this.willUnpause=!0),this.emit("paused",F),F?A||this.sound?.pause():A||this.sound?.resume()}step(){0==this.missiles.length&&this.lastMissileCantBeUndone&&(console.log("LASTMISSILECANTBEUNDONE = FALSE"),this.lastMissileCantBeUndone=!1),this.bodies=this.forceAccumulator(this.bodies);var F=this.detectCollision(this.bodies,this.missiles);if(this.bodies=F.bodies,this.missiles=F.missiles||[],this.missiles.length>0){let F=JSON.parse(JSON.stringify(this.missiles[0]));this.stillVisibleMissiles.push(F)}if(this.missiles.length>0&&0==this.missiles[0].radius)this.missiles.splice(0,1);else if(this.missiles.length>1&&0!==this.missiles[0].radius){let F=this.missiles.splice(0,1);this.missiles.splice(0,1,F[0])}return{bodies:this.bodies,missiles:this.missiles}}started(){this.emit("started",{day:this.day,level:this.level,bodyInits:JSON.parse(JSON.stringify(this.bodyInits))})}processMissileInits(F){return F.map(F=>{let A=this.convertFloatToScaledBigInt(this.missileVectorLimit);return{step:F.step,x:this.convertFloatToScaledBigInt(F.position.x).toString(),y:this.convertFloatToScaledBigInt(F.position.y).toString(),vx:(this.convertFloatToScaledBigInt(F.velocity.x)+A).toString(),vy:(this.convertFloatToScaledBigInt(F.velocity.y)+A).toString(),radius:"10"}})}finish(){if(this.finalBatchSent)return;let F=parseInt(this.convertFloatToScaledBigInt(this.missileVectorLimit)).toString();this.calculateBodyFinal();let A=[];if("game"==this.mode){let t=0;for(let e=this.alreadyRun;e